ai-chatbot/tests/routes/vip98k-reference-routing.test.ts

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([]);
});
});