import assert from 'node:assert/strict' import {selectDetailRegions,createRegionLoadingController} from '../../src/modules/cockpit/services/regionLoading.js' import {removeSceneTileset} from '../../src/modules/cockpit/services/tilesetCleanup.js' function event(){const f=new Set();return{addEventListener(fn){f.add(fn);return()=>f.delete(fn)},raise(...args){for(const fn of f)fn(...args)}}} function model(){return {tileLoad:event(),tileUnload:event(),tileVisible:event(),tileFailed:event(),loadProgress:event(),tilesLoaded:false}} const regions=Array.from({length:113},(_,id)=>({id,contentCount:30+(id%4)*20,retryAt:0,distance:10+id,active:false,url:'region'+id})) const wanted=selectDetailRegions(regions);assert.equal(wanted.length,2);assert.ok(wanted.reduce((s,i)=>s+regions[i].contentCount,0)<=180) assert.deepEqual(selectDetailRegions(regions,{pipeline:true}),[]) assert.deepEqual(selectDetailRegions(regions.map(r=>({...r,distance:1000}))),[]) const memory=selectDetailRegions(regions.map(r=>({...r,decodedGeometryBytes:60*1024*1024})));assert.equal(memory.length,1) const retained=selectDetailRegions(regions.map(r=>({...r,active:r.id===20})));assert.equal(retained[0],20) assert.deepEqual(selectDetailRegions([{id:0,active:true,distance:500,contentCount:20,retryAt:0},{id:1,active:false,distance:10,contentCount:20,retryAt:0}],{maxRegions:1}),[1],'new near region must outrank old distant active region') let started=[],added=[],removed=[],ticks=0 const models=new Map(),controller=createRegionLoadingController({regions:[{id:0,contentCount:2,expectedContentUrls:['a','b'],retryAt:0,url:'zero'},{id:1,contentCount:2,retryAt:0,url:'one'}], load:async r=>{started.push(r.id);const m=model();models.set(r.id,m);return m},add:m=>added.push(m),remove:m=>removed.push(m),distance:()=>10,now:()=>ticks, setTimer:()=>1,clearTimer:()=>{},requestRender:()=>{}}) controller.refresh();await Promise.resolve();await Promise.resolve();assert.deepEqual(started,[0],'only one initial stream') const a=models.get(0);assert.equal(a.show,false);assert.equal(a.preloadWhenHidden,true) a.tileLoad.raise({content:{url:'unrelated'}}) a.tileLoad.raise({content:{url:'a'}});a.tilesLoaded=true;controller.onRender();assert.equal(controller.detailReady(0),false,'one missing content preserves coarse');assert.equal(a.show,false);assert.deepEqual(started,[0]) a.tileLoad.raise({content:{url:'b'}});controller.onRender();await Promise.resolve();await Promise.resolve() assert.equal(a.show,true);assert.equal(controller.detailReady(0),false,'must observe rendered detail after hidden preload') a.tileVisible.raise({content:{trianglesLength:1}});controller.onRender() assert.equal(controller.detailReady(0),true);controller.refresh();assert.equal(controller.detailReady(0),true,'timers must not unmask between rendered frames');assert.deepEqual(started,[0,1],'second admitted only after actual complete first region') a.tileUnload.raise({content:{url:'b'}});assert.equal(controller.detailReady(0),false,'eviction must restore coarse') a.tileLoad.raise({content:{url:'b'}});a.tileVisible.raise({content:{trianglesLength:1}});controller.onRender();assert.equal(controller.detailReady(0),true) controller.setPipeline(true);assert.equal(controller.snapshot().length,0);assert.equal(controller.detailReady(0),false);assert.equal(removed.length,2,'pipeline releases all fine regions') controller.setPipeline(false);await Promise.resolve();await Promise.resolve();assert.ok(controller.snapshot().length<=2) const fresh=models.get(0);fresh.tileFailed.raise({message:'test failure'});controller.refresh();assert.equal(controller.detailReady(0),false) controller.detach(); let gate=false,front=0,frustumStarts=[],frustumRemoved=[] const frustum=createRegionLoadingController({regions:[{id:0,contentCount:1,retryAt:0},{id:1,contentCount:1,retryAt:0}],load:async r=>{frustumStarts.push(r.id);return model()},add:()=>{},remove:m=>frustumRemoved.push(m),canStart:()=>gate,distance:r=>r.id===front?10:Infinity,setTimer:()=>1,clearTimer:()=>{}}) frustum.refresh();await Promise.resolve();assert.deepEqual(frustumStarts,[],'coarse must finish before fine starts') gate=true;frustum.refresh();await Promise.resolve();await Promise.resolve();assert.deepEqual(frustumStarts,[0]) front=1;frustum.refresh();await Promise.resolve();await Promise.resolve();assert.deepEqual(frustumStarts,[0,1]);assert.equal(frustumRemoved.length,1,'offscreen region must release serial slot') frustum.detach(); // Late fromUrl completion after view switch must be destroyed, never added. let resolveLoad,lateAdded=0,lateRemoved=0 const late=createRegionLoadingController({regions:[{id:0,contentCount:1,retryAt:0}],load:()=>new Promise(r=>resolveLoad=r),add:()=>lateAdded++,remove:()=>lateRemoved++,distance:()=>1,setTimer:()=>1,clearTimer:()=>{}}) late.refresh();late.setPipeline(true);resolveLoad(model());await Promise.resolve();await Promise.resolve();assert.equal(lateAdded,0);assert.equal(lateRemoved,1);late.detach() let ranked=[] const ranking=createRegionLoadingController({regions:[{id:0,contentCount:1,retryAt:0},{id:1,contentCount:1,retryAt:0}],load:async r=>{ranked.push(r.id);return model()},add:()=>{},remove:()=>{},distance:r=>r.id?1:100,setTimer:()=>1,clearTimer:()=>{}}) ranking.refresh();await Promise.resolve();await Promise.resolve();assert.deepEqual(ranked,[1],'load must follow nearest priority, not manifest order');ranking.detach() let previewStarts=[],previewModels=new Map() const previews=createRegionLoadingController({regions:[{id:0,retryAt:0,contentCount:3,expectedContentUrls:['parent','fineA','fineB'],expectedPreviewContentUrls:['parent']},{id:1,retryAt:0,contentCount:1}], load:async r=>{previewStarts.push(r.id);const m=model();previewModels.set(r.id,m);return m},add:()=>{},remove:()=>{},distance:()=>1,setTimer:()=>1,clearTimer:()=>{}}) previews.refresh();await Promise.resolve();await Promise.resolve() const parent=previewModels.get(0);parent.tileLoad.raise({content:{url:'parent'}});parent.tilesLoaded=true;previews.onRender();await Promise.resolve();await Promise.resolve() assert.equal(parent.show,true);assert.deepEqual(previewStarts,[0,1],'complete visible preview releases serial slot without unused LOD leaves') parent.tileVisible.raise({content:{trianglesLength:1}});previews.onRender();assert.equal(previews.detailReady(0),true) assert.equal(previews.snapshot().find(r=>r.id===0).fineVisibleLastRender,false,'preview-only readiness is not fine rendering') parent.tileLoad.raise({content:{url:'fineA'}});previews.refresh();assert.equal(previews.snapshot().find(r=>r.id===0).fineLoaded,1) assert.equal(previews.snapshot().find(r=>r.id===0).fineVisibleLastRender,false,'loaded fine is not yet visible') parent.tileVisible.raise({content:{url:'fineA',trianglesLength:1}});previews.onRender();assert.equal(previews.snapshot().find(r=>r.id===0).fineVisibleLastRender,true) previews.onRender();assert.equal(previews.snapshot().find(r=>r.id===0).fineVisibleLastRender,false,'next rendered frame without fine clears visibility') parent.tileVisible.raise({content:{url:'parent',trianglesLength:1}});previews.onRender() parent.loadProgress.raise(1,1);assert.equal(previews.detailReady(0),true,'loaded preview preserves coarse coverage while fine waits') parent.tileUnload.raise({content:{url:'parent'}});assert.equal(previews.detailReady(0),false,'missing preview restores global coarse') previews.detach() let destroys=0 const unadded={isDestroyed:()=>destroys>0,destroy:()=>{destroys++}} const deadScene={isDestroyed:()=>true,get primitives(){throw Error('destroyed scene collection accessed')}} removeSceneTileset(deadScene,unadded);removeSceneTileset(deadScene,unadded);assert.equal(destroys,1) let collectionRemoved=0 removeSceneTileset({isDestroyed:()=>false,primitives:{remove(){collectionRemoved++;return true}}},{isDestroyed:()=>false,destroy(){throw Error('double destruction')}});assert.equal(collectionRemoved,1) console.log(JSON.stringify({passed:true,scope:'2-region/180-content admission, serial content readiness, unload/failure coarse restoration, pipeline release, late async completion'}))