import { expect, test } from "bun:test";
import { imageLayerBackgroundPatch, imageLayerExtractionTargets, imageLayerModelCalls, resolveImageLayerExtractions, validateImageLayerRegion, type ImageLayerExtraction } from "@/lib/canvas/canvas-image-layer-strategy";
import { parseImageLayerPlan } from "@/lib/canvas/canvas-image-layer-plan";
const region = { bbox: [100, 200, 600, 800] as [number, number, number, number], shape: "rect" as const };
const methods: ImageLayerExtraction[] = [{ method: "source" }, { method: "source-region", region }];
test("原图与模型混合按结构处理,原图全链路零图片调用", () => {
expect(resolveImageLayerExtractions(2, methods, [false, false])).toEqual(methods);
expect(imageLayerModelCalls(methods, true)).toBe(0);
expect(imageLayerModelCalls([{ method: "generate" }, methods[1], { method: "generate" }], true)).toBe(3);
expect(resolveImageLayerExtractions(2)).toEqual([{ method: "generate" }, { method: "generate" }]);
});
test("区域、角色、数量不合法时拒绝,显式缺失策略不能偷偷改为付费生成", () => {
expect(() => resolveImageLayerExtractions(2, methods, [false, true])).toThrow("不能同时移除");
expect(() => resolveImageLayerExtractions(2, [methods[1], methods[0]])).toThrow("第 1 层");
expect(() => resolveImageLayerExtractions(2, [methods[0], undefined as any])).toThrow("缺失");
expect(() => resolveImageLayerExtractions(2, [methods[0]])).toThrow("数量");
for (const bbox of [
[0, 0, 1001, 1000],
[400, 0, 300, 500],
[0, 0, NaN, 500],
[0, 0, 1, 100],
])
expect(() => validateImageLayerRegion({ ...region, bbox: bbox as any })).toThrow("区域无效");
expect(() => validateImageLayerRegion({ ...region, shape: "rounded-rect", radiusRatio: 1 })).toThrow("圆角");
});
test("只有所有待移除层均有原图区域时才限制背景生成范围", () => {
const generated: ImageLayerExtraction[] = [{ method: "generate" }, methods[1], { method: "generate" }];
expect(imageLayerBackgroundPatch({ dataUrl: "original" }, generated, [false, true, false])?.regions).toEqual([region]);
expect(imageLayerBackgroundPatch({ dataUrl: "original" }, generated, [false, true, true])).toBeUndefined();
expect(imageLayerBackgroundPatch({ dataUrl: "original" }, methods, [false, false])).toBeUndefined();
});
test("编辑区域后背景提示与本地实际提取使用同一坐标", () => {
const targets = imageLayerExtractionTargets(["底图", "面板:全部内容,区域 0 0 100 100"], methods);
expect(targets[1]).toBe("面板:全部内容,区域 100 200 600 800");
expect(targets[1].match(//g)).toHaveLength(1);
});
test("规划声明结构化原图策略,像素坐标按实际参考尺寸换算", () => {
const plan = parseImageLayerPlan(
JSON.stringify({
canPlan: true,
coordinateSpace: "pixels",
imageSize: { width: 1000, height: 500 },
layers: [
{ name: "完整底图", description: "保留全部内容", kind: "background", generation: { prompt: "保留完整原图", background: "opaque" }, extraction: { method: "source" } },
{
name: "规则区域",
description: "完整独立区域",
kind: "object",
generation: { prompt: "复制完整规则区域", background: "transparent" },
removeFromBackground: false,
bbox: [100, 100, 600, 400],
extraction: { method: "source-region", shape: "rounded-rect", radiusRatio: 0.05 },
},
],
}),
{ width: 1000, height: 500 },
);
expect(plan.layers[1].extraction?.region).toEqual({ bbox: [100, 200, 600, 800], shape: "rounded-rect", radiusRatio: 0.05 });
expect(
resolveImageLayerExtractions(
2,
plan.layers.map((layer) => layer.extraction!),
[false, false],
),
).toHaveLength(2);
});
test("无效可选坐标保留语义,但显式原图策略阻断付费回退", () => {
const plan = parseImageLayerPlan(
JSON.stringify({
canPlan: true,
layers: [
{ name: "底图", description: "完整底图", kind: "background", generation: { prompt: "保留环境并修补遮挡", background: "opaque" } },
{ name: "图形", description: "独立图形", kind: "object", generation: { prompt: "复制独立图形", background: "transparent" }, bbox: [0, 0, 1001, 500], extraction: { method: "source-region", shape: "rect" } },
],
}),
);
expect(plan.layers).toHaveLength(2);
expect(plan.layers[1].extraction?.method).toBe("source-region");
expect(() => resolveImageLayerExtractions(2, [{ method: "generate" }, plan.layers[1].extraction!])).toThrow("区域无效");
});