54 lines
5.6 KiB
JavaScript
54 lines
5.6 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import {readFileSync} from 'node:fs'
|
|
import * as Vue from 'vue'
|
|
import {compile} from '@vue/compiler-dom'
|
|
import {renderToString} from '@vue/server-renderer'
|
|
import ts from 'typescript'
|
|
import {folderAssets,reconcileSelection,activeJobs,previewHidden} from '../shared/studio2/viewer.mjs'
|
|
import {useAsHero,useAsSource,restoreAll} from '../shared/studio2/input.mjs'
|
|
import {toggleStyle,parseBatch,stylePrompt} from '../shared/studio2/styles.mjs'
|
|
import {compilePrompt,validateRequest} from '../shared/studio2/contracts.mjs'
|
|
import {cachedLoras} from '../shared/studio2/lora-cache.mjs'
|
|
|
|
test('deleted library files never return from job outputs; selection falls back or clears',()=>{
|
|
const deleted={id:'gone',folderId:'f'},keep={id:'keep',folderId:'f',createdAt:2};const jobs=[{request:{folderId:'f'},outputs:[deleted]}]
|
|
let assets=folderAssets({stills:[keep],clips:[]},jobs,'f');assert.deepEqual(assets.map(a=>a.id),['keep']);assert.equal(reconcileSelection(deleted,assets).id,'keep');assert.equal(reconcileSelection(deleted,[]),null)
|
|
})
|
|
test('bench includes only active execution phases and clear finished retains internal cleanup records',()=>{
|
|
assert.deepEqual(activeJobs(['waiting','waking','submitting','rendering','saving','complete','cancelled','failed'].map(state=>({state}))).map(j=>j.state),['waking','submitting','rendering','saving'])
|
|
const rows=[{id:'done',state:'complete',request:{folderId:'f'},cleanupPending:[{}]},{id:'active',state:'rendering',request:{folderId:'f'}}],saved=[]
|
|
const src=readFileSync(new URL('../server/api/studio-2/clear-finished.post.ts',import.meta.url),'utf8').replace(/^import .*;\r?\n/gm,'').replace('export default ','const handler=')
|
|
const js=ts.transpileModule(src,{compilerOptions:{target:ts.ScriptTarget.ES2022}}).outputText
|
|
const fn=new Function('defineEventHandler','assertLibraryOwner','listStudioJobs','records','assertFolderAccess','saveRecord',js+';return handler')(f=>f,()=>({owner:'o'}),()=>[],()=>rows,()=>{},r=>saved.push(r))
|
|
assert.equal(fn({}).cleared,1);assert.equal(rows[0].dismissed,true);assert.equal(rows[0].cleanupPending.length,1);assert.equal(rows[1].dismissed,undefined)
|
|
})
|
|
test('hidden xAIGen canvas and family render no image or video elements',async()=>{
|
|
const src=readFileSync(new URL('../components/studio2/Bench.vue',import.meta.url),'utf8')
|
|
const stage=src.slice(src.indexOf(' <div class="flex min-h-80'),src.indexOf(' <div v-if="selected"'))
|
|
const strip=src.split('\n').find(line=>line.includes('aria-label="Current family outputs"'))
|
|
const render=new Function('Vue',compile('<section>'+stage+strip+'</section>',{mode:'function',prefixIdentifiers:true}).code)(Vue)
|
|
const state={selected:{id:'a',kind:'still'},hideOutput:true,xaigen:true,family:[{id:'a',kind:'still'}],form:{hideInput:true},previewHidden,stillUrl:id=>'/image/'+id,selectOutput(){}}
|
|
const hidden=await renderToString(Vue.createSSRApp({render,data:()=>state}));assert.doesNotMatch(hidden,/<img|<video/);assert.match(hidden,/preview hidden/)
|
|
const visible=await renderToString(Vue.createSSRApp({render,data:()=>({...state,xaigen:false})}));assert.match(visible,/<img/)
|
|
assert.equal(previewHidden({role:'input'},true,true,false),true)
|
|
})
|
|
test('cached LoRAs survive sleeping GPU and failed explicit refresh',async()=>{
|
|
const cache={image:['Klein/portrait.safetensors'],video:['motion.safetensors']};let queried=0
|
|
const opts={read:()=>cache,write:()=>assert.fail('Do not erase cache'),live:async()=>{queried++;throw Error('Comfy asleep')}}
|
|
assert.deepEqual(await cachedLoras(opts),cache);assert.equal(queried,0);assert.deepEqual(await cachedLoras({...opts,fresh:true}),cache);assert.equal(queried,1)
|
|
})
|
|
test('source/hero actions are independent and full input restores structured settings',()=>{
|
|
const form={identityStillId:'hero',imageAId:'source',promptSections:{action:'old'},settings:{}}
|
|
assert.equal(useAsHero(form,{id:'new'}).imageAId,'source');assert.equal(useAsSource(form,{id:'new'}).identityStillId,'hero')
|
|
const saved={mode:'edit',identityStillId:'h',imageAId:'s',imageStyles:{positive:['Comic'],negative:['Skin pores']},promptSections:{action:'New',cameraBeats:[{moves:['hold']}]},settings:{steps:24,loraStack:[{name:'example.safetensors',strengthModel:.4,strengthClip:.6}]}}
|
|
const restored=restoreAll(form,{id:'result',studio2:saved});assert.equal(restored.imageAId,'result');assert.equal(restored.identityStillId,'h');assert.deepEqual(restored.settings.loraStack,saved.settings.loraStack);assert.deepEqual(restored.imageStyles,saved.imageStyles);assert.equal(restored.promptSections.action,'New');const clip=restoreAll(form,{id:'clip',kind:'clip',studio2:{...saved,mode:'video',engine:'minimax'}});assert.equal(clip.startClipId,'clip');assert.equal(clip.mode,'extend');assert.equal(clip.promptSections.action,'New')
|
|
})
|
|
test('image chips compile positive and negative independently; video chips do not leak',()=>{
|
|
let styles=toggleStyle({},'Comic');styles=toggleStyle(styles,'Skin pores',true);styles=toggleStyle(styles,'Comic',true);assert.equal(stylePrompt(styles),'');assert.match(stylePrompt(styles,true),/Comic/)
|
|
styles=toggleStyle(styles,'Photorealistic');const q=validateRequest({mode:'edit',engine:'flux',folderId:'f',imageAId:'s',imageStyles:styles,promptSections:{action:'Restaurant',cameraBeats:[{moves:['hold']}],locks:'VIDEO ONLY'}})
|
|
assert.match(q.compiledPrompt,/Photorealistic/);assert.doesNotMatch(q.compiledPrompt,/Comic|pores|tripod|hold|VIDEO ONLY/i);assert.deepEqual(q.imageStyles,styles)
|
|
assert.doesNotMatch(compilePrompt(q.promptSections,false,q),/Picture 1/)
|
|
assert.deepEqual(parseBatch('A wrapped\nsentence\n\nSecond shot\r\n\r\nThird'),['A wrapped\nsentence','Second shot','Third'])
|
|
})
|