import test from 'node:test'; import assert from 'node:assert/strict'; import {createRequire} from 'node:module'; import {createInspectionController,originalMediaDownload} from '../../web/js/workspace/inspection-state.mjs'; import {createViewerSession} from '../../web/js/workspace/viewer-state.mjs'; const require=createRequire(import.meta.url),{extractGenerationFacts}=require('../../web/js/comfy-metadata.js'); const m=await import('../../web/js/workspace/history-media.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND') throw e;return {};}); const epoch='11111111-1111-1111-1111-111111111111',run='22222222-2222-2222-2222-222222222222',scope='a'.repeat(64),location='b'.repeat(64); function media(node='1',ordinal=0,key=location) {const occurrence={run_id:run,output_node_id:node,image_ordinal:ordinal},reference={folder_type:'output',subfolder:'folder',filename:'a #%.png',path_style:'posix',root_key:'c'.repeat(64)},base=`/prompt_manager/history/runs/${run}/output/current`,query=new URLSearchParams({node,ordinal:String(ordinal),epoch,current_scope:scope});return {kind:'reported-location',occurrence,reference,history_epoch:epoch,current_scope:scope,location_key:key,filename:reference.filename,is_video:false,url:`${base}/media?${query}`,inspection_url:`${base}/inspection?${query}`,thumbnail_url:`${base}/thumbnail?${query}`};} const receipt=value=>({occurrence:value.occurrence,reference:value.reference,file_identity:'unverified',newly_generated:'unknown',context_membership:'unknown',transient:false,current_candidate:{status:'available',reason:null,evidence_class:'locator_candidate',current_scope:value.current_scope,location_key:value.location_key,relative_path:'folder/a #%.png',source_label:'output',gallery_match:'none',gallery_image_id:null,media:value}}); function envelope(target=media()) {return {success:true,inspection_version:1,history_version:1,history_epoch:epoch,current_output:{occurrence:target.occurrence,reference:target.reference,history_epoch:epoch,current_scope:scope,location_key:target.location_key,evidence_class:'locator_candidate',observation:'current-file',file_identity:'unverified',newly_generated:'unknown',context_membership:'unknown',transient:false},prompts:{positive:{status:'resolved',text:'current metadata only',source:'execution'},negative:{status:'unresolved',text:null,source:null}},file:{observation:'current-file',source_label:'output',media_type:'image',extension:'.png',format:'PNG',size:10,width:2,height:3,modified_time:1},metadata:{status:'absent',omitted_reason:null,raw:{},record_formats:{},prompt_graph:{status:'absent',value:null},workflow_graph:{status:'absent',value:null},parameters:null},resolution:{state:'absent',sampler_id:null,reason:'no graph'},library:{status:'none',scope:'saved-library',tags:[],metadata_scope:null,positive_id:null,pair_id:null,title:null,category:null}};} function adapter(){assert.ok(m.reportedLocationAdapter,'reported location adapter is exported');return m.reportedLocationAdapter;} test('reported location uses explicit occurrence routes and never widens rooted defaults',()=>{const target=media(),a=adapter();assert.doesNotThrow(()=>a.validate(target));assert.equal(a.key(target),location);assert.equal(a.scope(target),`${epoch}:${scope}`);assert.equal(a.inspectionPath(target),target.inspection_url);assert.deepEqual(a.originalDownload(target),{url:target.url,filename:'a #%.png'});assert.throws(()=>originalMediaDownload(target),TypeError);assert.throws(()=>createViewerSession({items:[target],initialKey:location}),TypeError);}); test('occurrence URLs reject endpoint query origin credentials fragments and identity ambiguities',()=>{const a=adapter(),target=media();for(const patch of [{url:target.url.replace('/media?','/thumbnail?')},{url:target.url+'&node=1'},{url:target.url+'&ordinal=0'},{url:target.url+'&epoch='+epoch},{url:target.url+'¤t_scope='+scope},{url:target.url+'&root=0'},{url:target.url+'#x'},{url:'https://attacker.test'+target.url},{url:'http://user:pass@localhost'+target.url},{url:target.url.replace('ordinal=0','ordinal=00')},{url:target.url.replace('node=1','node=2')},{inspection_url:target.url},{thumbnail_url:target.url},{history_epoch:'bad'},{current_scope:'A'.repeat(64)},{location_key:''},{occurrence:{...target.occurrence,image_ordinal:256}},{reference:{...target.reference,subfolder:'../bad'}}]) assert.throws(()=>a.validate({...target,...patch}),TypeError);const cached={...target,url:target.url+'&_pm_viewer=1'};assert.equal(a.originalDownload(cached).url,cached.url);}); test('proxy URLs retain exact configured prefix and reject unprefixed alternatives',()=>{const a=adapter(),previous=globalThis.window;globalThis.window={location:{href:'https://host.test/proxy/prompt_manager/workspace'},ApiClient:{resolveUrl:value=>value.startsWith('/prompt_manager/')?'/proxy'+value:value}};try {const target=media();assert.equal(a.originalDownload(target).url,'/proxy'+target.url);assert.throws(()=>a.originalDownload({...target,url:'https://host.test'+target.url}),TypeError);assert.throws(()=>a.originalDownload({...target,url:'https://host.test.attacker.test/proxy'+target.url}),TypeError);}finally{if(previous===undefined)delete globalThis.window;else globalThis.window=previous;}}); test('four repeated receipt coordinates form one loaded Viewer location retaining selected representative',()=>{assert.equal(typeof m.distinctCurrentMedia,'function');const values=[media('1',0),media('1',1),media('2',0),media('2',1)],receipts=values.map(receipt);assert.equal(new Set(receipts.map(r=>m.occurrenceKey(r.occurrence))).size,4);for(const chosen of [values[0],values[3]]) {const items=m.distinctCurrentMedia(receipts,chosen.occurrence);assert.equal(items.length,1);assert.deepEqual(items[0].occurrence,chosen.occurrence);}assert.deepEqual(m.distinctCurrentMedia(receipts,null)[0].occurrence,values[0].occurrence);const unavailable={...receipts[0],current_candidate:{status:'unavailable',reason:'missing',evidence_class:'locator_candidate'}};assert.deepEqual(m.distinctCurrentMedia([unavailable],null),[]);}); test('shared occurrence inspection keeps current facts and exports exact unverified evidence',async()=>{const a=adapter(),target=media('2',1),data=envelope(target);const record=a.toRecord(data,target,extractGenerationFacts);assert.deepEqual(record.identity,{kind:'reported-location',key:location,path:'folder/a #%.png',occurrence:target.occurrence,history_epoch:epoch,current_scope:scope,reference:target.reference});assert.equal(record.prompts.positive.text,'current metadata only');assert.equal(record.generation.seed.status,'not-recorded');assert.equal(record.identity.root_index,undefined);assert.deepEqual(record.download,{url:target.url,filename:target.filename});const {inspectionDownloads}=await import('../../web/js/workspace/file-inspector.mjs');const exported=JSON.parse(inspectionDownloads(record).metadata.bodyText);assert.deepEqual(exported.identity.occurrence,{run_id:run,output_node_id:'2',image_ordinal:1});assert.equal(exported.identity.history_epoch,epoch);assert.equal(exported.prompts.positive.text,'current metadata only');}); test('identity scope epoch or occurrence echo mismatches require refresh but malformed metadata is retryable',()=>{const a=adapter();for(const mutate of [p=>p.history_epoch='33333333-3333-3333-3333-333333333333',p=>p.current_output.current_scope='d'.repeat(64),p=>p.current_output.occurrence={...p.current_output.occurrence,image_ordinal:1},p=>p.current_output.reference={...p.current_output.reference,filename:'other.png'},p=>p.current_output.location_key='d'.repeat(64)]){const data=envelope();mutate(data);assert.throws(()=>a.toRecord(data,media(),extractGenerationFacts),e=>e.refreshRequired===true && typeof e.code==='string');}for(const mutate of [p=>p.success=false,p=>p.inspection_version=2,p=>p.metadata.parameters='fake',p=>p.current_output.file_identity='verified',p=>p.current_output.observation='historical-file']){const data=envelope();mutate(data);assert.throws(()=>a.toRecord(data,media(),extractGenerationFacts),e=>e instanceof TypeError && !e.refreshRequired);}}); test('shared inspection controller distinguishes recovery codes and obsolete receipt errors',async()=>{const a=adapter();for(const [status,code,refresh] of [[409,'history-changed',true],[409,'candidate-stale',true],[409,'capture-changed',true],[400,'invalid-request',false],[400,'invalid-cursor',false]]){const calls=[];const c=createInspectionController({adapter:a,extractFacts:extractGenerationFacts,api:{get:(url,{signal})=>new Promise((resolve,reject)=>calls.push({url,signal,resolve,reject}))}});const p=c.select(media());assert.equal(calls[0].url,media().inspection_url);calls[0].reject({status,code,recovery:{action:'reload-outputs'}});await p;assert.equal(c.state.refreshRequired,refresh);assert.equal(c.state.code,code);const retry=c.retry();assert.equal(calls.length,refresh?1:2);if(!refresh){calls[1].resolve(envelope());await retry;}c.dispose();}const calls=[],c=createInspectionController({adapter:a,extractFacts:extractGenerationFacts,api:{get:(url,{signal})=>new Promise((resolve,reject)=>calls.push({url,signal,resolve,reject}))}});const first=c.select(media('1',0)),second=c.select(media('1',1));assert.equal(calls.length,2,'same location needs exact occurrence selection');assert.equal(calls[0].signal.aborted,true);calls[1].resolve(envelope(media('1',1)));await second;calls[0].reject({status:409,code:'history-changed'});await first;assert.equal(c.state.record.identity.occurrence.image_ordinal,1);c.dispose();}); test('Viewer freezes selected occurrence and location entries through navigation and late timers',()=>{const a=adapter(),first=media('1',1),second=media('2',0,'d'.repeat(64)),items=[first,second],seen=[];const view=createViewerSession({adapter:a,items,initialKey:location,onChange:(state,item)=>seen.push([state,item])});first.occurrence.image_ordinal=9;first.reference.filename='mutated';items.reverse();assert.equal(seen[0][1].occurrence.image_ordinal,1);assert.equal(seen[0][1].reference.filename,'a #%.png');view.navigate(1);assert.equal(view.state.key,'d'.repeat(64));view.navigate(-1);assert.equal(seen.at(-1)[1].occurrence.image_ordinal,1);view.dispose();const count=seen.length;view.navigate(1);view.ready(location);view.play();assert.equal(seen.length,count);}); test('malformed occurrence echoes invalidate actions instead of producing retryable current facts',()=>{const a=adapter();for(const occurrence of [{run_id:run,output_node_id:'1',image_ordinal:'0'},{run_id:'bad',output_node_id:'1',image_ordinal:0},{}]){const data=envelope();data.current_output.occurrence=occurrence;assert.throws(()=>a.toRecord(data,media(),extractGenerationFacts),error=>error.refreshRequired===true && error.code==='candidate-stale');}}); test('stale inspection echoes use the canonical recovery vocabulary and identity status',()=>{ for(const [code,action] of [['history-changed','refresh-history'],['candidate-stale','reload-outputs']]) { const data=envelope(); if(code==='history-changed')data.history_epoch='33333333-3333-3333-3333-333333333333'; else data.current_output.current_scope='d'.repeat(64); assert.throws(()=>adapter().toRecord(data,media(),extractGenerationFacts),error=>{ assert.equal(error.code,code);assert.equal(error.status,409);assert.deepEqual(error.recovery,{action});return true; }); } });