import { describe, expect, test } from "bun:test"; import { buildAnimaWorkflow, buildSdxlWorkflow, buildZImageTurboWorkflow, buildZImageWorkflow, selectGeneratedOutputImage } from "./comfy"; describe("Comfy adapter", () => { test("selects SaveImage output instead of uploaded input or mask images", () => { const image = selectGeneratedOutputImage({ outputs: { "4": { images: [{ filename: "image-studio-input.png", type: "input" }] }, "9": { images: [{ filename: "image-studio-mask.png", type: "input" }] }, "8": { images: [{ filename: "image-studio-inpaint_00001_.png", subfolder: "", type: "output" }] }, }, }); expect(image).toEqual({ filename: "image-studio-inpaint_00001_.png", subfolder: "", type: "output" }); }); test("falls back to generated filename prefixes when node ids differ", () => { const image = selectGeneratedOutputImage({ outputs: { "12": { images: [{ filename: "image-studio-inpaint_00002_.png", type: "output" }] }, "4": { images: [{ filename: "image-studio-input.png", type: "input" }] }, }, }); expect(image?.filename).toBe("image-studio-inpaint_00002_.png"); }); test("builds neutral inpaint with VAEEncodeForInpaint", () => { const workflow = buildSdxlWorkflow(inpaintRequest({ maskedContent: "neutral" })); expect(workflow["5"]?.class_type).toBe("VAEEncodeForInpaint"); expect(workflow["5"]?.inputs).toMatchObject({ grow_mask_by: 6, mask: ["11", 0] }); expect(workflow["6"]?.inputs.latent_image).toEqual(["5", 0]); }); test("builds original-content inpaint with a latent noise mask", () => { const workflow = buildSdxlWorkflow(inpaintRequest({ maskedContent: "original", growMaskBy: 12 })); expect(workflow["5"]?.class_type).toBe("VAEEncode"); expect(workflow["12"]?.class_type).toBe("GrowMask"); expect(workflow["12"]?.inputs).toMatchObject({ mask: ["11", 0], expand: 12 }); expect(workflow["13"]?.class_type).toBe("SetLatentNoiseMask"); expect(workflow["13"]?.inputs).toMatchObject({ samples: ["5", 0], mask: ["12", 0] }); expect(workflow["6"]?.inputs.latent_image).toEqual(["13", 0]); }); test("builds Z-Image text-to-image with separated model loaders", () => { const workflow = buildZImageWorkflow(textRequest({ architecture: "z-image", model: "z_image_bf16.safetensors", steps: 30, cfg: 4 })); expect(workflow["1"]).toMatchObject({ class_type: "UNETLoader", inputs: { unet_name: "z_image_bf16.safetensors", weight_dtype: "default" } }); expect(workflow["2"]).toMatchObject({ class_type: "CLIPLoader", inputs: { clip_name: "qwen_3_4b.safetensors", type: "lumina2" } }); expect(workflow["6"]?.class_type).toBe("EmptySD3LatentImage"); expect(workflow["7"]).toMatchObject({ class_type: "ModelSamplingAuraFlow", inputs: { shift: 3 } }); expect(workflow["8"]?.inputs).toMatchObject({ steps: 30, cfg: 4, sampler_name: "res_multistep", scheduler: "simple", model: ["7", 0] }); expect(workflow["10"]?.class_type).toBe("SaveImage"); }); test("builds Z-Image Turbo with zeroed negative conditioning", () => { const workflow = buildZImageTurboWorkflow(textRequest({ architecture: "z-image-turbo", model: "z_image_turbo_bf16.safetensors", negativePrompt: "ignored" })); expect(workflow["1"]?.inputs.unet_name).toBe("z_image_turbo_bf16.safetensors"); expect(workflow["5"]).toMatchObject({ class_type: "ConditioningZeroOut", inputs: { conditioning: ["4", 0] } }); expect(workflow["8"]?.inputs).toMatchObject({ steps: 8, cfg: 1, sampler_name: "res_multistep", scheduler: "simple" }); }); test("builds Anima text-to-image workflow", () => { const workflow = buildAnimaWorkflow(textRequest({ architecture: "anima", model: "anima-base-v1.0.safetensors" })); expect(workflow["1"]).toMatchObject({ class_type: "UNETLoader", inputs: { unet_name: "anima-base-v1.0.safetensors" } }); expect(workflow["2"]).toMatchObject({ class_type: "CLIPLoader", inputs: { clip_name: "qwen_3_06b_base.safetensors", type: "stable_diffusion" } }); expect(workflow["3"]).toMatchObject({ class_type: "VAELoader", inputs: { vae_name: "qwen_image_vae.safetensors" } }); expect(workflow["6"]?.class_type).toBe("EmptyLatentImage"); expect(workflow["8"]?.inputs).toMatchObject({ steps: 30, cfg: 4, sampler_name: "er_sde", scheduler: "simple", model: ["1", 0] }); }); test("builds Anima with model-specific text encoder and VAE", () => { const workflow = buildAnimaWorkflow(textRequest({ architecture: "anima", model: "miaomiaoHarem_anima13.safetensors", textEncoder: "miaomiaoHarem_anima13_txt.safetensors", vae: "qwen_image_vae.safetensors", })); expect(workflow["1"]?.inputs.unet_name).toBe("miaomiaoHarem_anima13.safetensors"); expect(workflow["2"]?.inputs.clip_name).toBe("miaomiaoHarem_anima13_txt.safetensors"); expect(workflow["3"]?.inputs.vae_name).toBe("qwen_image_vae.safetensors"); }); }); function inpaintRequest(inpaint: { maskedContent: "neutral" | "original"; growMaskBy?: number }) { return { mode: "inpaint" as const, model: "model.safetensors", prompt: "replace garment", width: 128, height: 128, inputImage: "input.png", maskImage: "mask.png", inpaint, }; } function textRequest(overrides: Partial[0]> = {}) { return { architecture: "sdxl" as const, mode: "text-to-image" as const, model: "model.safetensors", prompt: "a studio portrait", negativePrompt: "low quality", width: 1024, height: 1024, seed: 123, ...overrides, }; }