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; }