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.alloc(128,'v'),downloadComfyImage:async()=>Buffer.from('image'),imageDimensions:()=>({width:960,height:960}), videoSourcePaths:()=>['source'],clipVideoPath:()=>'/fake/source',studio2Root:()=>'/fake',join,mkdirSync(){},writeFileSync(){},unlinkSync(){},probeDuration:async()=>5,stitchExtension:async()=>Buffer.alloc(128,'s'), 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('an empty Comfy video output fails the job with a real error and never saves or attaches a clip',async()=>{ const f=fixture();f.scope.downloadComfyVideo=async()=>Buffer.alloc(0) await runner(f.scope)(f.record,f.job) assert.equal(f.record.state,'failed');assert.match(f.record.error,/empty video file/);assert.equal(f.saved.length,0);assert.equal(f.record.outputs.length,0) }) test('a stitched extension with no readable duration fails the job and never saves or attaches a clip',async()=>{ const f=fixture('video');f.scope.probeDuration=async()=>NaN await runner(f.scope)(f.record,f.job) assert.equal(f.record.state,'failed');assert.match(f.record.error,/no readable duration/);assert.equal(f.saved.length,0);assert.equal(f.record.outputs.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/) })