import { expect, test } from "@playwright/test"; import sharp from "sharp"; import { resolveAlibabaExtendApiKey, resolveFalExtendApiKey } from "@/lib/ai/extend/config"; import { composeOutpaint, getOutpaintGeometry } from "@/lib/ai/extend/fal-outpaint"; test.describe("image extension provider configuration", () => { test("uses the most recently updated Alibaba image provider key", () => { expect( resolveAlibabaExtendApiKey( [ { apiKey: "older-key", updatedAt: "2026-09-01T00:00:00.000Z" }, { apiKey: "current-key", updatedAt: "2026-09-10T00:00:00.000Z" }, ], "stale-extend-setting-key", ), ).toBe("current-key"); }); test("only falls back to a legacy key when no provider key exists", () => { expect( resolveAlibabaExtendApiKey( [{ apiKey: "", updatedAt: "2026-09-10T00:00:00.000Z" }], "legacy-key", ), ).toBe("legacy-key"); expect(resolveAlibabaExtendApiKey([], "")).toBeNull(); }); test("uses an official fal provider key, not a key configured for a relay", () => { expect(resolveFalExtendApiKey([ { baseUrl: "https://relay.example.test", apiKey: "relay-key" }, { baseUrl: "https://queue.fal.run", apiKey: "Key official-key" }, ])).toBe("official-key"); }); }); test("outpainting restores every original pixel in place and keeps the generated border", async () => { const geometry = getOutpaintGeometry(13, 9, 1.5, 1.5); const { paddedWidth, paddedHeight, left, top } = geometry; const pixel = Buffer.from([42, 93, 190, 255, 30, 80, 20, 75]); const raw = Buffer.concat(Array.from({ length: 59 }, () => pixel)).subarray(0, 13 * 9 * 4); const source = await sharp(raw, { raw: { width: 13, height: 9, channels: 4 } }).png().toBuffer(); const expanded = await sharp({ create: { width: paddedWidth, height: paddedHeight, channels: 3, background: "#d8bc9e" } }).png().toBuffer(); const composed = await composeOutpaint(source, expanded, geometry); const area = await sharp(composed).extract({ left, top, width: 13, height: 9 }).raw().toBuffer(); expect(area.equals(raw)).toBe(true); expect([...await sharp(composed).extract({ left: 0, top: 0, width: 1, height: 1 }).raw().toBuffer()]).toEqual([216, 188, 158, 255]); await expect(composeOutpaint(source, source, geometry)).rejects.toThrow("尺寸不正确"); }); test("a 4K source keeps its target output size while the model works within its limit", () => { const g = getOutpaintGeometry(4096, 4096, 1.5, 1.5); expect([g.targetWidth, g.targetHeight, g.left, g.top]).toEqual([6144, 6144, 1024, 1024]); expect([g.paddedWidth, g.paddedHeight]).toEqual([2048, 2048]); expect(g.expandLeft + g.sourceWidth + g.expandRight).toBe(g.paddedWidth); expect(g.expandTop + g.sourceHeight + g.expandBottom).toBe(g.paddedHeight); expect(Math.abs(g.expandLeft - g.expandRight)).toBeLessThanOrEqual(1); });