56 lines
5.0 KiB
JavaScript
56 lines
5.0 KiB
JavaScript
import {queueSeeds} from '../shared/studio2/seed.mjs'
|
|
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import {readFileSync,mkdtempSync,mkdirSync,writeFileSync,existsSync} from 'node:fs'
|
|
import {tmpdir} from 'node:os'
|
|
import {join} from 'node:path'
|
|
import ts from 'typescript'
|
|
import {compilePrompt,validateRequest} from '../shared/studio2/contracts.mjs'
|
|
import {purgeStudio2Files} from '../scripts/studio2-purge.mjs'
|
|
const text=readFileSync(new URL('../server/utils/studio2/runner.ts',import.meta.url),'utf8')
|
|
const tree=ts.createSourceFile('runner.ts',text,ts.ScriptTarget.Latest,true)
|
|
function runner(scope){const node=tree.statements.find(n=>ts.isFunctionDeclaration(n)&&n.name?.text==='run');const js=ts.transpileModule(node.getText(tree),{compilerOptions:{target:ts.ScriptTarget.ES2022}}).outputText;return new Function(...Object.keys(scope),`${js};return run`)(...Object.values(scope))}
|
|
function fixture(mode='video') {
|
|
const phases=[],queued=[],saved=[],purged=[]
|
|
const request=validateRequest({folderId:'f',mode,engine:mode==='video'?'minimax':'flux',identityStillId:'hero',promptSections:{action:'walk'},batch:['turn','sit']})
|
|
const record={id:'r',owner:'o',request,outputs:[],queuedAt:1,startedAt:2,familyId:'family'}
|
|
const job={status:'running',clientId:'client',library:{}}
|
|
let id=0
|
|
const scope={
|
|
queueSeeds,watchProgress:()=>()=>{},compilePrompt,saveRecord(){},update(r,state){r.state=state;phases.push(state)},acquireSharedGpu:async()=>true,ensureComfyReady:async()=>{},emitJob(){},
|
|
prepareGraph:async r=>{queued.push({hero:r.request.identityStillId,start:r.request.startClipId,action:r.request.promptSections.action});return {}},queuePrompt:async()=>({prompt_id:`p${++id}`}),
|
|
fetchHistory:async p=>({[p]:{status:{status_str:'success'},outputs:{}}}),comfyFetch:()=>{throw new Error('Unexpected network call')},
|
|
extractVideo:()=>({filename:'video.mp4',subfolder:'preview/studio2/r',type:'output'}),extractEditedImage:()=>({filename:'image.png',subfolder:'preview/studio2/r',type:'output'}),
|
|
downloadComfyVideo:async()=>Buffer.from('video'),downloadComfyImage:async()=>Buffer.from('image'),imageDimensions:()=>({width:960,height:960}),
|
|
videoSourcePaths:()=>['source'],clipVideoPath:()=>'/fake/source',studio2Root:()=>'/fake',join,mkdirSync(){},stitchExtension:async()=>Buffer.from('stitched'),
|
|
saveClip:async p=>{const a={id:`asset${saved.length}`,...p};saved.push(a);return a},saveStill:async p=>{const a={id:`asset${saved.length}`,...p};saved.push(a);return a},
|
|
attachStudio2Metadata:async(owner,id,data)=>{assert.ok(saved.some(a=>a.id===id));assert.equal(data.identityStillId,'hero')},
|
|
purge:async r=>{assert.ok(saved.length>purged.length);purged.push(r.index);r.purgeResult='Cleared input + output'},
|
|
getClip:()=>saved.at(-1),getStill:()=>saved.at(-1),persistClipAnchorFrame:async()=>{},onLiveVideoSettled:async()=>{}
|
|
}
|
|
return {scope,record,job,phases,queued,saved,purged}
|
|
}
|
|
for(const mode of ['video','iterate']) test(`${mode} runner saves each output before purge and keeps identity through the batch`,async()=>{
|
|
const f=fixture(mode);await runner(f.scope)(f.record,f.job)
|
|
assert.equal(f.record.state,'complete');assert.equal(f.saved.length,3);assert.equal(f.purged.length,3)
|
|
assert.deepEqual(f.queued.map(q=>q.hero),['hero','hero','hero']);assert.deepEqual(f.queued.map(q=>q.action),['walk','turn','sit'])
|
|
if(mode==='video')assert.deepEqual(f.queued.map(q=>q.start),['','asset0','asset1'])
|
|
assert.deepEqual(f.phases.slice(0,4),['waking','submitting','rendering','saving'])
|
|
})
|
|
test('failed library save never purges host media',async()=>{
|
|
const f=fixture();f.scope.saveClip=async()=>{throw new Error('Disk full')};await runner(f.scope)(f.record,f.job);assert.equal(f.record.state,'failed');assert.equal(f.purged.length,0)
|
|
})
|
|
test('resume uses persisted Comfy prompt and never submits it again',async()=>{
|
|
const f=fixture();f.record.request.batch=[];f.record.promptId='existing';f.record.files=[];f.record.index=0
|
|
f.scope.queuePrompt=async()=>{throw new Error('Duplicate submit')};await runner(f.scope)(f.record,f.job)
|
|
assert.equal(f.record.state,'complete');assert.equal(f.queued.length,0);assert.equal(f.saved.length,1)
|
|
})
|
|
test('strict purge leaves production and root fallback files intact',()=>{
|
|
const root=mkdtempSync(join(tmpdir(),'studio2-purge-')),prefix='video/MiniMax_H3_preview',subfolder=prefix+'/studio2/job/0'
|
|
mkdirSync(join(root,subfolder),{recursive:true});mkdirSync(join(root,'video/MiniMax_H3'),{recursive:true})
|
|
for(const path of ['same.png','video/MiniMax_H3/same.png',subfolder+'/same.png'])writeFileSync(join(root,path),'image')
|
|
const result=purgeStudio2Files({prefix,files:[{filename:'same.png',subfolder,type:'output'}]},()=>[root])
|
|
assert.equal(result.cleared,1);assert.equal(existsSync(join(root,subfolder,'same.png')),false);assert.ok(existsSync(join(root,'same.png')));assert.ok(existsSync(join(root,'video/MiniMax_H3/same.png')))
|
|
assert.throws(()=>purgeStudio2Files({prefix,files:[{filename:'same.png',subfolder:'video/MiniMax_H3',type:'output'}]},()=>[root]),/outside/)
|
|
})
|