Files
aigen/tests/image-iterations.test.mjs
T

89 lines
5.9 KiB
JavaScript

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)
})