import assert from "node:assert/strict"; import { test } from "node:test"; import { selectCanvasReferenceImages } from "@/lib/canvas/reference-image-policy"; import { parseLayoutReplaceQuality } from "@/lib/canvas/layout-replace-quality"; import { runValidatedLayoutReplacement } from "@/lib/canvas/layout-replace-validation"; import { buildCanvasLayoutReplacePrompt, rankCanvasLayoutReplaceModels } from "@/lib/canvas/layout-replace-model-selection"; import { parseLayoutReplaceProductBox, layoutReplaceProductCrop } from "@/lib/canvas/layout-replace-product-crop"; const verdict = { sourceProductPreserved: true, templateLayoutPreserved: true, templateTextPreserved: true, wrongDirection: false, collage: false, confidence: .95, reasons: [] }; const quality = (changes: Record = {}) => parseLayoutReplaceQuality(JSON.stringify({ ...verdict, ...changes })); const image = (name: string) => ({ images: [{ originalUrl: `https://test.invalid/${name}.png`, thumbnailUrl: `https://test.invalid/${name}.png` }], model: "test-edit", modelRef: "test-edit", prompt: "test" }); test("a partial or entirely failed reachability probe never removes/reorders required image roles", () => { const requested = ["https://test.invalid/template.png", "https://test.invalid/product.png"]; for (const reachable of [[], [requested[0]], [requested[1]], [...requested].reverse()]) { assert.deepEqual(selectCanvasReferenceImages({ requested, reachable, requiredCount: 2 }), requested); } assert.deepEqual(selectCanvasReferenceImages({ requested, reachable: [requested[1]] }), [requested[1]]); assert.throws(() => selectCanvasReferenceImages({ requested: [requested[0]], reachable: [], requiredCount: 2 }), /必须提供 2 张/); }); test("template is the editing base and product identity is independent of furniture-specific facts", () => { const prompt = buildCanvasLayoutReplacePrompt({ referenceWidth: 750, referenceHeight: 1000, supplementalPrompt: "保持绿色柜门" }); assert.match(prompt, /图1是目标模版底图/); assert.match(prompt, /图2是新商品身份图/); assert.match(prompt, /不能把图1的旧商品搬入图2的背景/); assert.match(prompt, /750x1000/); assert.match(prompt, /保持绿色柜门/); assert.match(prompt, /不能把电商海报变成无字场景照片/); assert.doesNotMatch(prompt, /左侧带圆形拉手|四条桌腿|76%|缩短商品整体高度|扩大整体宽度/); }); test("the deployed GPT image alias is eligible, while single-image and unverified limits are excluded", () => { const candidate = (id: string, maxReferenceImages?: number) => ({ model: { id, modelId: id, supportsImageGen: true, supportsVision: true, healthStatus: "healthy", capabilities: { maxReferenceImages }, }, provider: { type: "vip98k" } }); const ranked = rankCanvasLayoutReplaceModels([candidate("nano-banana-2", 8), candidate("unknown"), candidate("single-edit", 1), candidate("gpt-image-2-vip", 8)]); assert.deepEqual(ranked.map(item => item.model.id), ["gpt-image-2-vip", "nano-banana-2"]); }); test("a healthy multi-image editor outranks a degraded preferred alias with an exhausted reseller account", () => { const ranked = rankCanvasLayoutReplaceModels([ { model: { id: "bad-account", modelId: "gpt-image-2", supportsImageGen: true, supportsVision: true, healthStatus: "degraded", capabilities: { maxReferenceImages: 8 } }, provider: { type: "bananaimage" } }, { model: { id: "healthy-editor", modelId: "nano-banana-2", supportsImageGen: true, supportsVision: true, healthStatus: "healthy", capabilities: { maxReferenceImages: 8 } }, provider: { type: "vip98k" } }, ]); assert.equal(ranked[0].model.id, "healthy-editor"); }); test("product focus crops preserve complete edges with outward padding and reject unreliable coordinates", () => { const box = parseLayoutReplaceProductBox('{"box":[100,500,900,850],"confidence":0.95}'); assert.deepEqual(layoutReplaceProductCrop(box, 1000, 1000), { left: 85, top: 485, width: 830, height: 380 }); assert.deepEqual(layoutReplaceProductCrop([0,0,1000,1000], 750, 1000), { left: 0, top: 0, width: 750, height: 1000 }); for (const value of ["null", "{}", '{"box":[100,500,900,850],"confidence":0.5}', '{"box":[100,500,90,850],"confidence":0.9}', '{"box":[-1,0,1000,1000],"confidence":0.9}', '{"box":[0,0,"1000",1000],"confidence":0.9}']) assert.throws(() => parseLayoutReplaceProductBox(value)); }); test("quality rejection covers identity, background direction, copy loss, collage and low confidence", () => { assert.equal(quality().passed, true); for (const changes of [{ sourceProductPreserved: false }, { templateLayoutPreserved: false }, { templateTextPreserved: false }, { wrongDirection: true }, { collage: true }, { confidence: .3 }]) { assert.equal(quality({ ...changes, passed: true }).passed, false); } }); test("incomplete or loosely typed verdicts never silently pass", () => { for (const value of ["null", "{}", "[]", "looks good", JSON.stringify({ ...verdict, confidence: "0.95" }), JSON.stringify({ ...verdict, confidence: 2 }), JSON.stringify({ ...verdict, sourceProductPreserved: "true" }), JSON.stringify({ ...verdict, reasons: [1] })]) { assert.throws(() => parseLayoutReplaceQuality(value), /质量检查/); } assert.equal(parseLayoutReplaceQuality("```json\n" + JSON.stringify(verdict) + "\n```").passed, true); }); test("a rejected first result generates one corrected candidate and only accepted output is persisted", async () => { const generated: string[] = []; const persisted: string[] = []; const verified: string[] = []; const result = await runValidatedLayoutReplacement({ generate: async (attempt, feedback) => { if (attempt) assert.match(feedback, /方向做反/); generated.push(`candidate-${attempt}`); return image(`candidate-${attempt}`); }, normalize: async result => ({ ...result, images: [{ originalUrl: result.images[0].originalUrl.replace(".png", "-normalized.png"), thumbnailUrl: "" }] }), verify: async result => { verified.push(result.images[0].originalUrl); return verified.length === 1 ? quality({ wrongDirection: true }) : quality(); }, persist: async result => { persisted.push(result.images[0].originalUrl); }, }); assert.equal(generated.length, 2); assert.equal(verified.length, 2); assert.ok(verified.every(url => url.endsWith("-normalized.png"))); assert.deepEqual(persisted, ["https://test.invalid/candidate-1-normalized.png"]); assert.equal(result.validation.attempts, 2); assert.equal(result.validation.status, "passed"); }); test("two rejected outputs finish as failure without any gallery/canvas success", async () => { let generations = 0; let saves = 0; await assert.rejects(runValidatedLayoutReplacement({ generate: async () => { generations++; return image("bad"); }, normalize: async result => result, verify: async () => quality({ sourceProductPreserved: false }), persist: async () => { saves++; }, }), /未通过质量检查/); assert.equal(generations, 2); assert.equal(saves, 0); }); test("unknown generation failure is not replayed and a verifier outage cannot trigger paid regeneration", async () => { for (const failure of ["generation", "verifier"]) { let generations = 0; let saves = 0; await assert.rejects(runValidatedLayoutReplacement({ generate: async () => { generations++; if (failure === "generation") throw new Error("outcome-unknown timeout"); return image("unverified"); }, normalize: async result => result, verify: async () => { throw new Error("quality service unavailable"); }, persist: async () => { saves++; }, })); assert.equal(generations, 1); assert.equal(saves, 0); } }); test("a passing first result is saved once without an unnecessary second generation", async () => { let generations = 0; let saves = 0; const result = await runValidatedLayoutReplacement({ generate: async () => { generations++; return image("good"); }, normalize: async result => result, verify: async () => quality(), persist: async () => { saves++; }, }); assert.equal(generations, 1); assert.equal(saves, 1); assert.equal(result.validation.attempts, 1); });