import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import ts from 'typescript' function load(path, mocks = {}) { const source = readFileSync(new URL('../' + path, import.meta.url), 'utf8') const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText const exports = {} new Function('require', 'exports', code)(id => { if (!(id in mocks)) throw Error('Unexpected dependency ' + id) return mocks[id] }, exports) return exports } const iterations = load('utils/imageIterations.ts') const { normalizeImageIterations: normalize, resolveImageIteration: resolve, imageIterationSweep: sweep } = iterations test('requested sweep produces four ordered variations with one comparison seed', () => { const rows = sweep('24, 20, 18, 19', '1.5') assert.deepEqual(rows.map(row => row.steps), [24,20,18,19]) assert.ok(rows.every(row => row.cfg === 1.5 && row.seed === 'same')) assert.equal(sweep('20 24', '1, 1.5').length, 4) }) test('blank overrides inherit, explicit zero and no-LoRA remain explicit', () => { const pass = normalize([{ prompt: '', steps: '', cfg: 0, seed: 0, loraStack: [] }])[0] assert.equal(pass.steps, undefined) assert.deepEqual(pass.loraStack, []) assert.deepEqual(resolve({steps:24,cfg:1.5,seed:42},pass), {steps:24,cfg:0,seed:0,strength:undefined}) assert.equal(resolve({steps:24,cfg:1.5,seed:42},{prompt:'',seed:'same'}).seed,42) }) test('malformed settings reject visibly instead of being silently discarded', () => { for (const raw of ['bad JSON', {}, [null], [{steps:51}], [{steps:1.5}], [{cfg:-1}], [{seed:-1}], [{strength:2}], [{loraStack:[{name:'x',strengthModel:3}]}], Array(50).fill({})]) { assert.throws(() => normalize(raw)) } assert.throws(() => sweep('24, potato', '1.5')) assert.throws(() => sweep('', '1.5')) }) test('iteration runner sends per-image settings to Comfy and saves matching metadata', async () => { const built = [], saved = [] const job = {id:'test', clientId:'client', status:'queued', progress:0, library:{ownerKey:'owner',prompt:'shared prompt',promptMid:'shared prompt',name:'test',folderId:'inbox'}} const baseStack = [{ name:'base.safetensors', strengthModel:1, strengthClip:1 }] const customStack = [{ name:'custom.safetensors', strengthModel:0.5, strengthClip:0.5 }] const mocks = { '~/utils/imageIterations': iterations, '~/server/utils/jobs': {createJob:()=>job,emitJob:()=>{}}, '~/server/utils/comfyLifecycle': {ensureComfyReady:async()=>{}}, '~/server/utils/comfy': {assertImageScaleToTotalPixelsNode:async()=>{},comfyImagePrefix:()=> 'test',getComfyHost:()=> 'fake',uploadImage:async()=>({name:'input'}),queuePrompt:async()=>({prompt_id:'p'+built.length})}, '~/server/utils/imageComfy': {withImageComfyHost:async(_,fn)=>fn(),waitForImageEdit:async()=>({filename:'out.png'}),downloadEditedImage:async()=>Buffer.from('test'),purgeImageComfyArtifacts:async()=>{}}, '~/server/utils/imageWorkflowV2': {buildImageV2Workflow:params=>{built.push(params);return {graph:{},workflowFile:'fake'}},IMAGE_V2_NODE_LABELS:{},resolveKreaGenerateAssets:async()=>({})}, '~/server/utils/loras': {ensureComfyLoraNames:async()=>{},persistLoraFields:stack=>({loraStack:stack})}, '~/server/utils/resolution': {imageDimensions:()=>({width:1024,height:1024})}, '~/server/utils/watch': {emitChainJob:()=>{}}, '~/server/utils/library': {saveStill:async params=>{assert.equal(job.saving,true);assert.equal(job.library.chainContinuing,true);saved.push(params);return {id:'s'+saved.length,filename:'out.png'}},stillChainName:(_,i)=>'image'+i}, '~/utils/outputNames': {comfyJobPrefix:()=> 'test'}, '~/utils/promptParts': {restorePromptParts:prompt=>({prompt}),wrappedPromptForComfy:(_,prompt)=>prompt}, '~/utils/videoModels': {isXaigenStudio:()=>false}, '~/utils/imageV2': {imageV2GraphMode:()=> 'generate',imageV2StackSpecials:()=>({}),mergeLegacyImageV2Loras:stack=>stack || [],parseImageV2Engine:()=> 'flux'}, '~/server/utils/imagePad': {applyImageFill:async({image})=>({image,padded:false})}, '~/utils/imageAspect': {composeFillPrompt:prompt=>prompt}, '~/server/utils/studioQueue': {onLiveVideoSettled:async()=>{assert.equal(job.saving,false);assert.equal(job.library.chainContinuing,false)}} } const {runEditV2} = load('server/utils/imageChainV2.ts', mocks) await runEditV2(job,{ mode:'iterate',task:'t2i',prompt:'shared prompt',negative:'',steps:24,cfg:1.5,seed:42,loraStack:baseStack, snofsModel:0,snofsClip:0,consistencyModel:0,consistencyClip:0,megapixels:0,reference:null, passes:[{prompt:'',steps:20,seed:'same'},{prompt:'alternate',steps:18,cfg:2,seed:0,loraStack:customStack},{prompt:'',steps:19,seed:'same',loraStack:[]}] }) assert.equal(job.status,'complete',job.error) assert.deepEqual(built.map(p=>p.steps),[24,20,18,19]) assert.deepEqual(built.map(p=>p.cfg),[1.5,1.5,2,1.5]) assert.deepEqual(built.map(p=>p.seed),[42,42,0,42]) assert.deepEqual(built.map(p=>p.prompt),['shared prompt','shared prompt','alternate','shared prompt']) assert.deepEqual(built.map(p=>p.loraStack),[baseStack,baseStack,customStack,[]]) assert.deepEqual(saved.map(p=>p.settings.steps),[24,20,18,19]) assert.deepEqual(saved.map(p=>p.settings.loraStack),[baseStack,baseStack,customStack,[]]) }) test('editing a queued batch preserves its per-variation settings', () => { const queued = load('utils/queuedJob.ts', { '~/utils/loras': {persistLoraFields:()=>({}),readLoraStack:()=>[]}, '~/utils/music': {} }) const payload = {prompt:'shared',passes:[{prompt:'',steps:18,cfg:1.5,seed:'same',loraStack:[]}]} const shots = queued.queuedJobShots(payload,'edit') shots[1].steps = 19 const restored = queued.applyQueuedShots('edit',shots,payload) assert.deepEqual(restored.passes,[{prompt:'',steps:19,cfg:1.5,seed:'same',loraStack:[]}]) assert.deepEqual(normalize(JSON.stringify(restored.passes)),restored.passes) })