392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
bindVip98kImages,
|
|
resolveVip98kTaskInvocation,
|
|
selectVip98kTaskInvocation,
|
|
type Vip98kTaskInvocation,
|
|
} from "@/lib/ai/vip98k-task-invocation";
|
|
import {
|
|
buildVip98kModelPresets,
|
|
isVip98kImageModelVisible,
|
|
} from "@/lib/ai/vip98k-models";
|
|
import {
|
|
addVip98kReferenceRetryToken,
|
|
getStorageObjectKeyFromPublicUrl,
|
|
getVip98kStagedMediaRetryDelay,
|
|
getVip98kSubmissionRetryDelay,
|
|
shouldStageVip98kReferenceUrl,
|
|
} from "@/lib/ai/vip98k-reference-staging";
|
|
import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery";
|
|
|
|
const textToImageInvocation: Vip98kTaskInvocation = {
|
|
kind: "tool",
|
|
method: "POST",
|
|
path: "/api/external/tools/generate?tool_id=google%2Fnano-banana-2%2Ftext-to-image",
|
|
contentType: "application/json",
|
|
requestExample: { prompt: "Generate an image" },
|
|
resultMethod: "GET",
|
|
resultPath:
|
|
"/api/external/tools/result?tool_id=google%2Fnano-banana-2%2Ftext-to-image&taskId={task_id}",
|
|
};
|
|
|
|
test.describe("VIP98K reference-image routing", () => {
|
|
test("identifies default-storage references that need provider staging", () => {
|
|
expect(
|
|
shouldStageVip98kReferenceUrl({
|
|
mediaUrl: "https://default.example/uploads/reference.jpg",
|
|
sourcePublicUrl: "https://default.example",
|
|
targetPublicUrl: "https://backup.example/project",
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldStageVip98kReferenceUrl({
|
|
mediaUrl: "https://backup.example/project/uploads/reference.jpg",
|
|
sourcePublicUrl: "https://default.example",
|
|
targetPublicUrl: "https://backup.example/project",
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
shouldStageVip98kReferenceUrl({
|
|
mediaUrl: "https://external.example/reference.jpg",
|
|
sourcePublicUrl: "https://default.example",
|
|
targetPublicUrl: "https://backup.example/project",
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
getStorageObjectKeyFromPublicUrl(
|
|
"https://default.example/assets/folder%20name/reference.jpg?token=secret",
|
|
"https://default.example/assets",
|
|
),
|
|
).toBe("folder name/reference.jpg");
|
|
});
|
|
|
|
test("retries transient media rejection while a staged URL propagates", () => {
|
|
expect(
|
|
getVip98kStagedMediaRetryDelay({
|
|
status: 400,
|
|
message: "The media request is invalid.",
|
|
attempt: 1,
|
|
maxAttempts: 3,
|
|
stagedReferenceCount: 2,
|
|
}),
|
|
).toBe(20_000);
|
|
expect(
|
|
getVip98kStagedMediaRetryDelay({
|
|
status: 400,
|
|
message: "The media request is invalid.",
|
|
attempt: 3,
|
|
maxAttempts: 3,
|
|
stagedReferenceCount: 2,
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
addVip98kReferenceRetryToken(
|
|
"https://cdn.example/reference.jpg?existing=1",
|
|
"retry-2",
|
|
),
|
|
).toBe(
|
|
"https://cdn.example/reference.jpg?existing=1&sg_media_retry=retry-2",
|
|
);
|
|
});
|
|
|
|
test("retries provider reconciliation without treating it as a media failure", () => {
|
|
expect(
|
|
getVip98kSubmissionRetryDelay({
|
|
status: 503,
|
|
message: "The media request is pending safe reconciliation.",
|
|
attempt: 1,
|
|
maxAttempts: 3,
|
|
}),
|
|
).toBe(15_000);
|
|
expect(
|
|
getVip98kSubmissionRetryDelay({
|
|
status: 503,
|
|
message: "pending safe reconciliation",
|
|
attempt: 2,
|
|
maxAttempts: 3,
|
|
retryAfterMs: 8_000,
|
|
}),
|
|
).toBe(8_000);
|
|
expect(
|
|
getVip98kSubmissionRetryDelay({
|
|
status: 503,
|
|
message: "pending safe reconciliation",
|
|
attempt: 3,
|
|
maxAttempts: 3,
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
test("shows the newly published Nano Banana Pro image editor", () => {
|
|
expect(
|
|
isVip98kImageModelVisible({
|
|
modelId: "nano-banana-pro",
|
|
type: "image",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("replaces a text-to-image alias invocation with the resolved reference tool", () => {
|
|
const invocation = resolveVip98kTaskInvocation(
|
|
"google/nano-banana-2/reference-to-image",
|
|
textToImageInvocation
|
|
);
|
|
|
|
expect(invocation?.path).toContain(
|
|
"tool_id=google%2Fnano-banana-2%2Freference-to-image"
|
|
);
|
|
expect(invocation?.resultPath).toContain(
|
|
"tool_id=google%2Fnano-banana-2%2Freference-to-image"
|
|
);
|
|
expect(invocation?.requestExample).toMatchObject({
|
|
prompt: "",
|
|
video_clips: [{ fps: 1, url: "", ends: 0, start: 0 }],
|
|
});
|
|
});
|
|
|
|
test("keeps the catalog invocation when it already targets the resolved tool", () => {
|
|
const catalogInvocation: Vip98kTaskInvocation = {
|
|
...textToImageInvocation,
|
|
path: "/api/external/tools/generate?tool_id=google%2Fnano-banana-2%2Freference-to-image",
|
|
resultPath:
|
|
"/api/external/tools/result?tool_id=google%2Fnano-banana-2%2Freference-to-image&taskId={task_id}",
|
|
requestExample: {
|
|
prompt: "Catalog prompt",
|
|
video_clips: [{ fps: 2, url: "catalog", ends: 1, start: 0 }],
|
|
},
|
|
};
|
|
|
|
expect(
|
|
resolveVip98kTaskInvocation(
|
|
"google/nano-banana-2/reference-to-image",
|
|
catalogInvocation
|
|
)
|
|
).toBe(catalogInvocation);
|
|
});
|
|
|
|
test("does not rewrite unrelated tools", () => {
|
|
expect(
|
|
resolveVip98kTaskInvocation("other/image-model", textToImageInvocation)
|
|
).toBe(textToImageInvocation);
|
|
});
|
|
|
|
test("routes remove-background through the dedicated external tool", () => {
|
|
const invocation = resolveVip98kTaskInvocation("remove-background", null);
|
|
|
|
expect(invocation?.path).toBe(
|
|
"/api/external/tools/generate?tool_id=remove-background"
|
|
);
|
|
expect(invocation?.resultPath).toBe(
|
|
"/api/external/tools/result?tool_id=remove-background&taskId={task_id}"
|
|
);
|
|
expect(invocation?.requestExample).toEqual({ image_url: "" });
|
|
});
|
|
|
|
test("routes enhance through the dedicated external tool", () => {
|
|
const invocation = resolveVip98kTaskInvocation("enhance", null);
|
|
|
|
expect(invocation?.path).toBe(
|
|
"/api/external/tools/generate?tool_id=enhance"
|
|
);
|
|
expect(invocation?.requestExample).toEqual({
|
|
image_url: "",
|
|
scale_factor: 4,
|
|
});
|
|
});
|
|
|
|
test("selects the V2 media-task contract when a model receives reference images", () => {
|
|
const generationInvocation: Vip98kTaskInvocation = {
|
|
kind: "tool",
|
|
method: "POST",
|
|
path: "/v1/images/generations",
|
|
contentType: "application/json",
|
|
requestExample: { model: "nano-banana-2", n: 1, prompt: "Generate" },
|
|
resultMethod: "GET",
|
|
resultPath: "",
|
|
};
|
|
const editInvocation: Vip98kTaskInvocation = {
|
|
kind: "task",
|
|
method: "POST",
|
|
path: "/extensions/v1/tasks",
|
|
contentType: "application/json",
|
|
requestExample: {
|
|
file_urls: ["https://api.vip98k.com/logo.png"],
|
|
model: "nano-banana-2",
|
|
prompt: "Edit",
|
|
},
|
|
resultMethod: "GET",
|
|
resultPath: "/extensions/v1/tasks/{task_id}/result",
|
|
};
|
|
|
|
expect(
|
|
selectVip98kTaskInvocation({
|
|
modelId: "nano-banana-2",
|
|
configuredInvocation: generationInvocation,
|
|
configuredInvocations: [generationInvocation, editInvocation],
|
|
imageCount: 2,
|
|
}),
|
|
).toBe(editInvocation);
|
|
});
|
|
|
|
test("binds every reference image to the V2 file_urls field", () => {
|
|
const body: Record<string, unknown> = {
|
|
file_urls: ["https://api.vip98k.com/logo.png"],
|
|
model: "nano-banana-2",
|
|
prompt: "Edit",
|
|
};
|
|
|
|
expect(
|
|
bindVip98kImages(body, [
|
|
"https://example.com/a.png",
|
|
"https://example.com/b.png",
|
|
]),
|
|
).toEqual(["file_urls"]);
|
|
expect(body.file_urls).toEqual([
|
|
"https://example.com/a.png",
|
|
"https://example.com/b.png",
|
|
]);
|
|
});
|
|
|
|
test("keeps V2 processing-task contracts instead of rewriting them to retired routes", () => {
|
|
const invocation: Vip98kTaskInvocation = {
|
|
kind: "task",
|
|
method: "POST",
|
|
path: "/extensions/v1/tasks",
|
|
contentType: "application/json",
|
|
requestExample: {
|
|
file_urls: ["https://api.vip98k.com/logo.png"],
|
|
model: "enhance",
|
|
},
|
|
resultMethod: "GET",
|
|
resultPath: "/extensions/v1/tasks/{task_id}/result",
|
|
};
|
|
|
|
expect(resolveVip98kTaskInvocation("enhance", invocation)).toBe(invocation);
|
|
});
|
|
|
|
test("derives multi-image support and parameter controls from all V2 invocations", () => {
|
|
const [preset] = buildVip98kModelPresets([
|
|
{
|
|
id: "nano-banana-2",
|
|
type: "image",
|
|
capabilities: ["openai.images.generations", "text_to_image"],
|
|
invocation: {
|
|
kind: "tool",
|
|
method: "POST",
|
|
path: "/v1/images/generations",
|
|
content_type: "application/json",
|
|
request_example: { model: "nano-banana-2", n: 1, prompt: "Generate" },
|
|
},
|
|
invocations: [
|
|
{
|
|
kind: "tool",
|
|
method: "POST",
|
|
path: "/v1/images/generations",
|
|
content_type: "application/json",
|
|
request_example: { model: "nano-banana-2", n: 1, prompt: "Generate" },
|
|
},
|
|
{
|
|
kind: "task",
|
|
method: "POST",
|
|
path: "/extensions/v1/tasks",
|
|
content_type: "application/json",
|
|
request_example: {
|
|
file_urls: ["https://api.vip98k.com/logo.png"],
|
|
model: "nano-banana-2",
|
|
prompt: "Edit",
|
|
},
|
|
result_method: "GET",
|
|
result_path: "/extensions/v1/tasks/{task_id}/result",
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
expect(preset).toMatchObject({
|
|
supportsVision: true,
|
|
supportsImageGen: true,
|
|
capabilities: {
|
|
supportsMultiImageReference: true,
|
|
maxReferenceImages: 8,
|
|
supportedImageSizes: [],
|
|
supportedAspectRatios: ["auto"],
|
|
},
|
|
imageGenConfig: {
|
|
supportsSize: false,
|
|
supportedImageSizes: [],
|
|
supportedAspectRatios: ["auto"],
|
|
},
|
|
});
|
|
expect(
|
|
(preset?.imageGenConfig?.vip98k as { invocations?: unknown[] })?.invocations,
|
|
).toHaveLength(2);
|
|
});
|
|
|
|
test("detects V2 asset-only results that need delivery URL resolution", () => {
|
|
expect(
|
|
shouldResolveVip98kResultDelivery({
|
|
payload: {
|
|
ready: true,
|
|
result: {
|
|
result_available: true,
|
|
outputs: [{ asset_id: "opaque", type: "image" }],
|
|
},
|
|
status: "succeeded",
|
|
},
|
|
status: "succeeded",
|
|
outputUrlCount: 0,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldResolveVip98kResultDelivery({
|
|
payload: { status: "succeeded" },
|
|
status: "succeeded",
|
|
outputUrlCount: 1,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("excludes text-only VIP98K image models from editing workflows", () => {
|
|
const [preset] = buildVip98kModelPresets([
|
|
{
|
|
id: "nano-banana-pro",
|
|
type: "image",
|
|
capabilities: ["openai.images.generations", "text_to_image"],
|
|
invocation: {
|
|
kind: "tool",
|
|
method: "POST",
|
|
path: "/v1/images/generations",
|
|
content_type: "application/json",
|
|
request_example: { model: "nano-banana-pro", n: 1, prompt: "Generate" },
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(preset).toBeUndefined();
|
|
});
|
|
|
|
test("does not inherit multi-image support from broad authorized metadata", () => {
|
|
const presets = buildVip98kModelPresets([
|
|
{
|
|
id: "nano-banana-2-lite",
|
|
type: "image",
|
|
capabilities: ["text_to_image", "image_to_image"],
|
|
inputModes: ["text", "image"],
|
|
invocation: {
|
|
kind: "tool",
|
|
method: "POST",
|
|
path: "/v1/images/generations",
|
|
content_type: "application/json",
|
|
request_example: {
|
|
model: "nano-banana-2-lite",
|
|
n: 1,
|
|
prompt: "Generate",
|
|
},
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(presets).toEqual([]);
|
|
});
|
|
});
|