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, parseLayoutReplaceProductAnalysis, layoutReplaceProductCrop, layoutReplaceProductAspectRatio } from "@/lib/canvas/layout-replace-product-crop"; import { makeLayoutReplaceQualityBoard } from "@/lib/canvas/layout-replace-quality-board"; import { parseLayoutReplaceSupport, compareLayoutReplaceSupport } from "@/lib/canvas/layout-replace-product-support"; import { parseLayoutReplaceTextBoxes, parseLayoutReplaceTextCheck } from "@/lib/canvas/layout-replace-text-lock"; 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.equal(layoutReplaceProductAspectRatio(box, 1000, 1000), 800 / 350); 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("verification board visibly separates old template, true source and final result without stretching", async () => { const sharp = (await import("sharp")).default; const make = (color: string) => sharp({ create: { width: 100, height: 100, channels: 3, background: color } }).png().toBuffer(); const board = await makeLayoutReplaceQualityBoard({ template: await make("red"), source: await make("blue"), product: await make("green"), result: await make("yellow") }); const { data, info } = await sharp(board).raw().toBuffer({ resolveWithObject: true }); const pixel = (x: number, y: number) => [...data.subarray((y*info.width+x)*info.channels,(y*info.width+x)*info.channels+3)]; assert.deepEqual([info.width,info.height], [1390,1390]); assert.deepEqual(pixel(190,282), [255,0,0]); assert.deepEqual(pixel(190,60), [255,255,255]); assert.deepEqual(pixel(190,752), [0,0,255]); assert.deepEqual(pixel(885,217), [0,128,0]); assert.deepEqual(pixel(885,912), [255,255,0]); }); test("observed product structure is required and carried into the generation constraints", () => { const identity = "木框绿色柜,贴地底框,无柜脚,左右有开放格"; const analysis = parseLayoutReplaceProductAnalysis(JSON.stringify({ box: [0,0,1000,1000], confidence: .95, identity })); assert.equal(analysis.identity, identity); assert.match(buildCanvasLayoutReplacePrompt({ sourceProductIdentity: identity }), /贴地底框,无柜脚/); assert.match(buildCanvasLayoutReplacePrompt({ sourceProductIdentity: identity }), /不能继承图1的腿/); assert.throws(() => parseLayoutReplaceProductAnalysis('{"box":[0,0,1000,1000],"confidence":0.95}'), /结构识别不完整/); }); test("template text regions reject missing, invalid and whole-poster replacement masks", () => { assert.deepEqual(parseLayoutReplaceTextBoxes('{"containsText":false,"boxes":[]}'), []); assert.deepEqual(parseLayoutReplaceTextBoxes('{"containsText":true,"boxes":[[0,0,200,100]]}'), [[0,0,200,100]]); for (const value of ["null", "{}", '{"containsText":true,"boxes":[]}', '{"containsText":false,"boxes":[[0,0,200,100]]}', '{"containsText":true,"boxes":[[0,0,1000,1000]]}', '{"containsText":true,"boxes":[[100,0,20,100]]}']) assert.throws(() => parseLayoutReplaceTextBoxes(value)); }); test("text verification refuses discount/price changes, uncertain reading and incomplete conclusions", () => { const good = { identical: true, uncertain: false, confidence: .95, differences: [] }; assert.equal(parseLayoutReplaceTextCheck(JSON.stringify(good)).passed, true); assert.equal(parseLayoutReplaceTextCheck(JSON.stringify({ ...good, passed: true, differences: ["8.7折变成6.7折"] })).passed, false); assert.equal(parseLayoutReplaceTextCheck(JSON.stringify({ ...good, uncertain: true })).passed, false); assert.equal(parseLayoutReplaceTextCheck(JSON.stringify({ ...good, identical: false })).passed, false); for (const value of ["null", "{}", JSON.stringify({ ...good, confidence: "0.95" }), JSON.stringify({ ...good, differences: [1] })]) { assert.throws(() => parseLayoutReplaceTextCheck(value)); } }); test("structured OCR differences preserve numeric changes and discard unchanged rows", () => { const changed = parseLayoutReplaceTextCheck(JSON.stringify({ identical: true, uncertain: false, confidence: .95, differences: [{ original: "消费券8.7折起", result: "消费券6.7折起" }, { original: "满8000减1050元", result: "满8000减1050元" }] })); assert.equal(changed.passed, false); assert.equal(changed.differences.length, 1); assert.match(changed.differences[0], /8.7.*6.7/); const equivalent = parseLayoutReplaceTextCheck(JSON.stringify({ identical: true, uncertain: false, confidence: .95, differences: [{ original: "价格 1188", result: "价格1188" }] })); assert.equal(equivalent.passed, true); }); test("independently observed support types reject added legs without confusing template legs with results", () => { const observed = (supportType: string, confidence = .95) => parseLayoutReplaceSupport(JSON.stringify({ supportType, confidence })); assert.equal(compareLayoutReplaceSupport(observed("flush"), observed("flush")).passed, true); assert.equal(compareLayoutReplaceSupport(observed("flush"), observed("legs")).passed, false); assert.equal(compareLayoutReplaceSupport(observed("legs"), observed("flush")).passed, false); assert.equal(compareLayoutReplaceSupport(observed("suspended"), observed("legs")).passed, false); assert.equal(compareLayoutReplaceSupport(observed("not_applicable"), observed("unclear")).passed, true); assert.equal(compareLayoutReplaceSupport(observed("flush"), observed("unclear")).passed, false); assert.equal(compareLayoutReplaceSupport(observed("flush"), observed("flush", .3)).passed, false); assert.throws(() => parseLayoutReplaceSupport('{"supportType":"maybe","confidence":0.95}')); }); 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); });