123 lines
4.9 KiB
TypeScript
123 lines
4.9 KiB
TypeScript
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
createImageTemplate,
|
|
deleteImageTemplate,
|
|
listImageTemplates,
|
|
updateImageTemplate
|
|
} from "@/lib/server/data-store";
|
|
import { normalizeImageTemplateCreate } from "@/lib/server/image-template-input";
|
|
import { extractMaterialPlaceholders } from "@/lib/prompt/material-placeholders";
|
|
|
|
let runtimeDir = "";
|
|
let previousRuntimeDir: string | undefined;
|
|
let previousSupabaseUrl: string | undefined;
|
|
let previousSupabaseKey: string | undefined;
|
|
|
|
describe("image templates", () => {
|
|
beforeEach(async () => {
|
|
runtimeDir = await mkdtemp(join(tmpdir(), "zhinian-templates-"));
|
|
previousRuntimeDir = process.env.ZHINIAN_RUNTIME_DIR;
|
|
previousSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
previousSupabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
process.env.ZHINIAN_RUNTIME_DIR = runtimeDir;
|
|
delete process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
delete process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
restoreEnv("ZHINIAN_RUNTIME_DIR", previousRuntimeDir);
|
|
restoreEnv("NEXT_PUBLIC_SUPABASE_URL", previousSupabaseUrl);
|
|
restoreEnv("SUPABASE_SERVICE_ROLE_KEY", previousSupabaseKey);
|
|
await rm(runtimeDir, { force: true, recursive: true });
|
|
});
|
|
|
|
it("stores templates inside the account owner boundary", async () => {
|
|
const first = await createImageTemplate({
|
|
ownerId: "auth:app:user-a",
|
|
name: "商品主图",
|
|
description: "适合电商主视觉",
|
|
prompt: "生成干净高级的商品主图",
|
|
previewImageUrl: "/mock/preview-a.png",
|
|
settings: { width: 2048, height: 2048, forceSingle: true },
|
|
sortOrder: 2
|
|
});
|
|
await createImageTemplate({
|
|
ownerId: "auth:app:user-b",
|
|
name: "社媒海报",
|
|
prompt: "生成适合社媒传播的海报",
|
|
settings: {},
|
|
sortOrder: 1
|
|
});
|
|
|
|
const owned = await listImageTemplates("auth:app:user-a");
|
|
expect(owned).toHaveLength(1);
|
|
expect(owned[0]).toMatchObject({
|
|
id: first.id,
|
|
ownerId: "auth:app:user-a",
|
|
description: "适合电商主视觉",
|
|
prompt: "生成干净高级的商品主图",
|
|
settings: { width: 2048, height: 2048, forceSingle: true }
|
|
});
|
|
expect(await listImageTemplates("auth:app:user-b")).toHaveLength(1);
|
|
});
|
|
|
|
it("does not update or delete another account's template", async () => {
|
|
const template = await createImageTemplate({
|
|
ownerId: "auth:app:user-a",
|
|
name: "A",
|
|
prompt: "提示词 A",
|
|
settings: {},
|
|
sortOrder: 0
|
|
});
|
|
|
|
expect(await updateImageTemplate(template.id, "auth:app:user-b", { name: "B" })).toBeNull();
|
|
expect(await deleteImageTemplate(template.id, "auth:app:user-b")).toBeNull();
|
|
expect((await listImageTemplates("auth:app:user-a"))[0].name).toBe("A");
|
|
|
|
const updated = await updateImageTemplate(template.id, "auth:app:user-a", { name: "A+", sortOrder: 3 });
|
|
expect(updated).toMatchObject({ name: "A+", sortOrder: 3 });
|
|
expect(await deleteImageTemplate(template.id, "auth:app:user-a")).toMatchObject({ id: template.id });
|
|
expect(await listImageTemplates("auth:app:user-a")).toHaveLength(0);
|
|
});
|
|
|
|
it("normalizes template creation input", () => {
|
|
expect(normalizeImageTemplateCreate({
|
|
name: " 品牌氛围图 ",
|
|
description: " 品牌视觉模板 ",
|
|
prompt: " 生成品牌氛围视觉 ",
|
|
previewImageUrl: "https://example.com/preview.png",
|
|
settings: { engine: "evolink", width: "1440", height: "2560", forceSingle: false, quality: "high", scale: "999" },
|
|
sortOrder: "5"
|
|
})).toMatchObject({
|
|
name: "品牌氛围图",
|
|
description: "品牌视觉模板",
|
|
prompt: "生成品牌氛围视觉",
|
|
previewImageUrl: "https://example.com/preview.png",
|
|
settings: { engine: "evolink", width: 1440, height: 2560, forceSingle: false, quality: "high", scale: 100 },
|
|
sortOrder: 5
|
|
});
|
|
expect(() => normalizeImageTemplateCreate({ name: "", prompt: "x" })).toThrow("模板名称不能为空");
|
|
expect(() => normalizeImageTemplateCreate({ name: "x", prompt: "x", previewImageUrl: "javascript:alert(1)" })).toThrow("效果预览图地址");
|
|
});
|
|
|
|
it("extracts prompt material placeholders for template upload slots", () => {
|
|
expect(extractMaterialPlaceholders("以 @图片1 为主体,参考 @图2 的色调,再参考 @视频1 的运动感。")).toEqual([
|
|
{ token: "@图片1", type: "image", index: 1 },
|
|
{ token: "@图片2", type: "image", index: 2 },
|
|
{ token: "@视频1", type: "video", index: 1 }
|
|
]);
|
|
expect(extractMaterialPlaceholders("以 @图片 为主体,@图片这种普通文字不应变成占位。")).toEqual([]);
|
|
});
|
|
});
|
|
|
|
function restoreEnv(name: string, value: string | undefined) {
|
|
if (value === undefined) {
|
|
delete process.env[name];
|
|
return;
|
|
}
|
|
process.env[name] = value;
|
|
}
|