import { expect, test } from "@playwright/test"; import { bindVip98kImages, resolveVip98kTaskInvocation, resolveVip98kImageTaskSize, selectVip98kTaskInvocation, type Vip98kTaskInvocation, } from "@/lib/ai/vip98k-task-invocation"; import { buildVip98kModelPresets, isVip98kImageModelVisible, } from "@/lib/ai/vip98k-models"; import { addVip98kReferenceRetryToken, getStorageObjectKeyFromPublicUrl, getVip98kReferenceStagingObjectKey, getVip98kStagedMediaRetryDelay, getVip98kSubmissionRetryDelay, isVip98kStagedReferenceUrl, shouldStageVip98kReferenceUrl, } from "@/lib/ai/vip98k-reference-staging"; import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery"; import { normalizeVip98kRuntimeConfig, resolveVip98kSubmissionTimeoutMs, } from "@/lib/ai/vip98k-runtime-config"; import { BANANAIMAGE_MODEL_PRESETS } from "@/lib/ai/bananaimage-models"; 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("keeps every restored BananaImage preset multi-reference capable", () => { expect(BANANAIMAGE_MODEL_PRESETS.map((preset) => preset.modelId)).toEqual([ "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview-plus", "gemini-3.1-flash-image-preview-plus", "gpt-image-2", ]); expect( BANANAIMAGE_MODEL_PRESETS.every( (preset) => preset.supportsImageGen && preset.supportsVision && preset.capabilities?.supportsMultiImageReference === true && Number(preset.capabilities?.maxReferenceImages) >= 2, ), ).toBe(true); }); test("uses a stable cross-process cache key for staged references", () => { const source = "https://storage.example/uploads/reference.png"; const key = getVip98kReferenceStagingObjectKey(source); expect(key).toMatch(/^uploads\/vip98k-cache\/[a-f0-9]{64}\.jpg$/); expect( getVip98kReferenceStagingObjectKey(`${source}?sg_media_retry=retry-2`), ).toBe(key); expect( isVip98kStagedReferenceUrl( `https://minio.example/bucket/${key}`, ), ).toBe(true); }); test("loads bounded database runtime settings with a Pro timeout override", () => { const defaults = normalizeVip98kRuntimeConfig(null); expect(resolveVip98kSubmissionTimeoutMs("nano-banana-2", defaults)).toBe( 120_000, ); expect(resolveVip98kSubmissionTimeoutMs("nano-banana-pro", defaults)).toBe( 240_000, ); expect(defaults.referenceCacheTtlMs).toBe(7 * 24 * 60 * 60 * 1000); const config = normalizeVip98kRuntimeConfig({ submissionTimeoutMs: 90_000, modelSubmissionTimeoutMs: { "nano-banana-pro": 300_000 }, maxSubmissionAttempts: 20, referenceCacheTtlMs: 1, }); expect(config.maxSubmissionAttempts).toBe(5); expect(config.referenceCacheTtlMs).toBe(5 * 60_000); expect(resolveVip98kSubmissionTimeoutMs("nano-banana-2", config)).toBe( 90_000, ); expect(resolveVip98kSubmissionTimeoutMs("NANO-BANANA-PRO", config)).toBe( 300_000, ); }); 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: "The request was sent, but its final outcome could not be confirmed. Do not retry with a new idempotency key.", attempt: 1, maxAttempts: 3, }), ).toBe(15_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 = { 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("binds every reference image to an array-shaped image field", () => { const body: Record = { image: ["https://api.vip98k.com/logo.png"], model: "gpt-image-2-vip", prompt: "Edit", }; expect( bindVip98kImages(body, [ "https://example.com/a.png", "https://example.com/b.png", ]), ).toEqual(["image"]); expect(body.image).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", }, input_schema: { properties: { aspect_ratio: { type: "string" }, file_urls: { type: "array", maxItems: 8 }, resolution: { type: "string" }, size: { type: "string", enum: ["auto", "1024x1024"] }, }, }, result_method: "GET", result_path: "/extensions/v1/tasks/{task_id}/result", }, ], }, ]); expect(preset).toMatchObject({ supportsVision: true, supportsImageGen: true, capabilities: { supportsMultiImageReference: true, maxReferenceImages: 8, supportedImageSizes: ["1K", "2K", "4K"], supportedAspectRatios: [ "auto", "1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "4:5", "5:4", ], }, imageGenConfig: { supportsSize: true, supportedImageSizes: ["1K", "2K", "4K"], }, }); expect( (preset?.imageGenConfig?.vip98k as { invocations?: unknown[] })?.invocations, ).toHaveLength(2); expect( ( (preset?.imageGenConfig?.vip98k as { invocations?: Array }) ?.invocations?.[1]?.inputSchema as Record )?.properties, ).toBeTruthy(); }); 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 models from the image picker", () => { const presets = buildVip98kModelPresets([ { id: "gpt-image-2-vip", 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: "gpt-image-2-vip", n: 1, prompt: "Generate" }, }, }, ]); expect(presets).toEqual([]); }); test("keeps gpt-image-2-vip when its published image field is an array", () => { const presets = buildVip98kModelPresets([ { id: "gpt-image-2-vip", type: "image", capabilities: [ "image_to_image", "openai.images.generations", "text_to_image", ], invocation: { kind: "tool", method: "POST", path: "/v1/images/generations", content_type: "application/json", request_example: { image: ["https://api.vip98k.com/logo.png"], model: "gpt-image-2-vip", n: 1, prompt: "Generate", }, }, }, ]); expect(presets).toHaveLength(1); expect(presets[0]).toMatchObject({ modelId: "gpt-image-2-vip", supportsVision: true, isHidden: false, capabilities: { supportsMultiImageReference: true, maxReferenceImages: 8, }, }); }); 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([]); }); test("excludes Lite and Fast even when catalog metadata claims image arrays", () => { const presets = buildVip98kModelPresets( ["nano-banana-2-lite", "nano-banana-fast"].map((modelId) => ({ id: modelId, type: "image", capabilities: ["text_to_image", "image_to_image"], invocation: { kind: "task", method: "POST", path: "/extensions/v1/tasks", content_type: "application/json", request_example: { file_urls: ["https://api.vip98k.com/logo.png"], model: modelId, prompt: "Edit", }, input_schema: { properties: { file_urls: { type: "array", maxItems: 8 }, }, }, }, })), ); expect(presets).toEqual([]); }); test("maps frontend ratio and resolution to the VIP98K V2 size contract", () => { expect( resolveVip98kImageTaskSize({ imageSize: "1K", aspectRatio: "16:9", fallbackSize: "auto", }), ).toBe("1024x576"); expect( resolveVip98kImageTaskSize({ imageSize: "886*1182", aspectRatio: "3:4", fallbackSize: "auto", }), ).toBe("886x1182"); expect( resolveVip98kImageTaskSize({ imageSize: "1K", aspectRatio: "auto", fallbackSize: "auto", }), ).toBe("auto"); }); });