ai-chatbot/tests/routes/video-canvas-tools.test.ts

54 lines
3.5 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { promptToolSchema, selectPromptToolModel, promptToolErrorMessage } from "@/lib/video-canvas/prompt-tools";
import { toVideoTimeRange, videoProcessSchema } from "@/lib/video-canvas/media-contract";
import { normalizeVideoCanvasFunctionConfig } from "@/lib/video-canvas/config";
test("prompt tools reject empty and oversized input before calling a model", () => {
expect(promptToolSchema.safeParse({ mode: "translate", prompt: " " }).success).toBe(false);
expect(promptToolSchema.safeParse({ mode: "optimize", prompt: "a".repeat(12001) }).success).toBe(false);
expect(promptToolSchema.parse({ mode: "translate", prompt: " @角色1 保持 8 秒 " }).prompt).toBe("@角色1 保持 8 秒");
});
test("prompt tools exclude media models and never silently replace an explicit model", () => {
const image = { id: "image", modelId: "gpt-image-2", supportsImageGen: true };
const video = { id: "video", modelId: "veo-3.1-fast", capabilities: { kind: "video" } };
const text = { id: "text", modelId: "qwen3.8-max", isEnabled: true };
expect(selectPromptToolModel([image, video, text], "auto")).toEqual(text);
expect(selectPromptToolModel([text], "qwen3.8-max")).toEqual(text);
expect(() => selectPromptToolModel([image, video, text], "image")).toThrow("文字模型");
expect(() => selectPromptToolModel([{ ...text, isEnabled: false }], "text")).toThrow();
expect(() => selectPromptToolModel([text], "missing")).toThrow();
});
test("prompt tools preserve administrator model and prompt overrides", () => {
const config = normalizeVideoCanvasFunctionConfig({ defaultMultimodalModel: "qwen38", functions: {
"video-prompt-translate": { enabled: false, model: "explicit-model", prompt: "Translate exactly." },
} });
expect(config.functions["video-prompt-optimize"].route).toBe("canvas-prompt-tools");
expect(config.functions["video-prompt-translate"]).toMatchObject({ enabled: false, model: "explicit-model", prompt: "Translate exactly." });
expect(config.defaultMultimodalModel).toBe("qwen38");
});
test("upstream errors give an actionable message without returning provider internals", () => {
expect(promptToolErrorMessage(new Error("Access denied, account in good standing overdue-payment"))).toContain("账户");
expect(promptToolErrorMessage(new Error("timed out"))).toContain("超时");
expect(promptToolErrorMessage(new Error("private upstream URL"))).not.toContain("private");
});
test("trim uses source duration rather than assuming eight seconds", () => {
expect(toVideoTimeRange({ start: 0.25, end: 0.75 }, 8)).toEqual({ start: 2, end: 6, duration: 4 });
expect(toVideoTimeRange({ start: 0.1, end: 0.5 }, 20)).toEqual({ start: 2, end: 10, duration: 8 });
});
test("invalid trim and crop requests are rejected before downloading", () => {
const url = "https://example.com/video.mp4";
for (const range of [{ start: 0.5, end: 0.5 }, { start: -0.1, end: 1 }, { start: 0, end: 2 }]) {
expect(videoProcessSchema.safeParse({ operation: "trim", url, range }).success).toBe(false);
}
expect(videoProcessSchema.safeParse({ operation: "trim", url }).success).toBe(false);
expect(videoProcessSchema.safeParse({ operation: "crop", url, rect: { x: 0.8, y: 0, width: 0.8, height: 1 } }).success).toBe(false);
expect(() => toVideoTimeRange({ start: 0, end: 0.001 }, 8)).toThrow("0.1 秒");
expect(() => toVideoTimeRange({ start: 0, end: 1 }, NaN)).toThrow();
expect(videoProcessSchema.safeParse({ operation: "audio-video", url }).success).toBe(true);
});