import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import { register } from 'node:module' register('./t32-adapter-io-loader.mjs',import.meta.url) const { createCesiumPipeModels }=await import('../../../src/modules/cockpit/services/cesiumPipeModels.js') globalThis.t32Models=[] globalThis.t32TerrainFails=false globalThis.window={location:{href:'http://localhost:8002/cockpit/',origin:'http://localhost:8002'}} const originalFetch=globalThis.fetch let missing=false const requested=[] globalThis.fetch=async(input,options)=>{ const url=String(input) if(url.startsWith('blob:')) return originalFetch(input,options) requested.push(url) if(missing) return new Response('missing',{status:404}) const name=url.split('/').at(-1) const json=await readFile(new URL('../../../public/cockpit/models/pipes/'+name,import.meta.url),'utf8') return new Response(json,{status:200}) } const source={entities:{values:[]}} const added=[],removed=[] const viewer={terrainProvider:{},isDestroyed:()=>false,scene:{requestRender(){},primitives:{ add(model){added.push(model)},remove(model){removed.push(model);model.destroy()}, }}} const states=[] let adapter=createCesiumPipeModels(viewer,{source,onChange:state=>states.push(state),base:'/',viewMode:'pipeline'}) const pipe=(code,lon)=>({code,industry:'GAS',diameter:400,positions:[[lon,28.44],[lon+.00004,28.44]]}) const network={pipes:[pipe('east',110.391),pipe('west',110.411)],components:[]} source.entities.values=network.pipes.map(row=>({id:'pipe:'+row.code,show:true})) await adapter.sync(network) assert.equal(added.length,2) assert.equal(states.at(-1).pending,2) assert.ok(source.entities.values.every(e=>e.show),'load promise does not remove fallback') for(const model of added) model.readyEvent.raiseEvent() assert.ok(source.entities.values.every(e=>!e.show),'ready switches complete pipes to models') assert.equal(requested.filter(url=>url.endsWith('spline.gltf')).length,1) assert.equal(added[0].gltf.buffers[0].uri,added[1].gltf.buffers[0].uri) assert.equal(added[0].gltf.buffers[0].uri,'http://localhost:8002/cockpit/models/pipes/spline.bin') await adapter.sync({...network,pipes:network.pipes.map(row=>({...row,name:'changed metadata'}))}) assert.equal(added.length,2,'metadata-only change does not rebuild GPU batches') await adapter.sync({...network,pipes:[{...network.pipes[0],diameter:600},network.pipes[1]]}) assert.equal(added.length,3,'only changed tile recreated') assert.equal(removed.length,1) assert.equal(source.entities.values[0].show,true) assert.equal(source.entities.values[1].show,false) added[2].readyEvent.raiseEvent() added[2].errorEvent.raiseEvent(new Error('late buffer failure')) assert.equal(source.entities.values[0].show,true) assert.equal(states.at(-1).failed,1) adapter.destroy() assert.ok(added.every(model=>model.destroyed)) console.log('PASS real adapter readiness, shared URL, stable metadata, selective update, late error and cleanup') const guide = { id:'pipe:guide', show:true, polyline:{} } source.entities.values=[guide] adapter=createCesiumPipeModels(viewer,{source,base:'/',viewMode:'pipeline'}) const guideNetwork={pipes:[pipe('guide',110.451)],components:[]} await adapter.sync(guideNetwork) added.at(-1).readyEvent.raiseEvent() assert.equal(guide.polyline.distanceDisplayCondition.near,600) guide.polyline={} await adapter.sync({pipes:[{...guideNetwork.pipes[0],name:'metadata-only'}],components:[]}) assert.equal(guide.polyline.distanceDisplayCondition?.near,600,'replaced polyline must retain near-model/far-guide rule') adapter.destroy() console.log('PASS replacement guide graphics receive distance restriction again') const terrainStates=[] globalThis.t32TerrainFails=true source.entities.values=[{id:'pipe:terrain',show:true}] adapter=createCesiumPipeModels(viewer,{source,onChange:state=>terrainStates.push(state),base:'/'}) await adapter.sync({pipes:[pipe('terrain',110.431)],components:[]}) assert.equal(terrainStates.at(-1).terrainFailed,true) assert.equal(terrainStates.at(-1).fallback,1) assert.equal(source.entities.values[0].show,true) adapter.destroy() console.log('PASS terrain failure preserves fallback and publishes reason') globalThis.t32TerrainFails=false missing=true const assetStates=[] source.entities.values=[{id:'component:404',show:true}] adapter=createCesiumPipeModels(viewer,{source,onChange:state=>assetStates.push(state),base:'missing-base/'}) await adapter.sync({pipes:[],components:[{code:'404',kind:'valve',industry:'GAS',position:[110.431,28.44]}]}) assert.equal(assetStates.at(-1).failed,1) assert.equal(assetStates.at(-1).fallback,1) assert.equal(source.entities.values[0].show,true) adapter.destroy() globalThis.fetch=originalFetch console.log('PASS template 404 reaches real adapter failure path and keeps symbol')