t32-adapter-behavior.mjs 4.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import assert from 'node:assert/strict'
  2. import { readFile } from 'node:fs/promises'
  3. import { register } from 'node:module'
  4. register('./t32-adapter-io-loader.mjs',import.meta.url)
  5. const { createCesiumPipeModels }=await import('../../../src/modules/cockpit/services/cesiumPipeModels.js')
  6. globalThis.t32Models=[]
  7. globalThis.t32TerrainFails=false
  8. globalThis.window={location:{href:'http://localhost:8002/cockpit/',origin:'http://localhost:8002'}}
  9. const originalFetch=globalThis.fetch
  10. let missing=false
  11. const requested=[]
  12. globalThis.fetch=async(input,options)=>{
  13. const url=String(input)
  14. if(url.startsWith('blob:')) return originalFetch(input,options)
  15. requested.push(url)
  16. if(missing) return new Response('missing',{status:404})
  17. const name=url.split('/').at(-1)
  18. const json=await readFile(new URL('../../../public/cockpit/models/pipes/'+name,import.meta.url),'utf8')
  19. return new Response(json,{status:200})
  20. }
  21. const source={entities:{values:[]}}
  22. const added=[],removed=[]
  23. const viewer={terrainProvider:{},isDestroyed:()=>false,scene:{requestRender(){},primitives:{
  24. add(model){added.push(model)},remove(model){removed.push(model);model.destroy()},
  25. }}}
  26. const states=[]
  27. let adapter=createCesiumPipeModels(viewer,{source,onChange:state=>states.push(state),base:'/',viewMode:'pipeline'})
  28. const pipe=(code,lon)=>({code,industry:'GAS',diameter:400,positions:[[lon,28.44],[lon+.00004,28.44]]})
  29. const network={pipes:[pipe('east',110.391),pipe('west',110.411)],components:[]}
  30. source.entities.values=network.pipes.map(row=>({id:'pipe:'+row.code,show:true}))
  31. await adapter.sync(network)
  32. assert.equal(added.length,2)
  33. assert.equal(states.at(-1).pending,2)
  34. assert.ok(source.entities.values.every(e=>e.show),'load promise does not remove fallback')
  35. for(const model of added) model.readyEvent.raiseEvent()
  36. assert.ok(source.entities.values.every(e=>!e.show),'ready switches complete pipes to models')
  37. assert.equal(requested.filter(url=>url.endsWith('spline.gltf')).length,1)
  38. assert.equal(added[0].gltf.buffers[0].uri,added[1].gltf.buffers[0].uri)
  39. assert.equal(added[0].gltf.buffers[0].uri,'http://localhost:8002/cockpit/models/pipes/spline.bin')
  40. await adapter.sync({...network,pipes:network.pipes.map(row=>({...row,name:'changed metadata'}))})
  41. assert.equal(added.length,2,'metadata-only change does not rebuild GPU batches')
  42. await adapter.sync({...network,pipes:[{...network.pipes[0],diameter:600},network.pipes[1]]})
  43. assert.equal(added.length,3,'only changed tile recreated')
  44. assert.equal(removed.length,1)
  45. assert.equal(source.entities.values[0].show,true)
  46. assert.equal(source.entities.values[1].show,false)
  47. added[2].readyEvent.raiseEvent()
  48. added[2].errorEvent.raiseEvent(new Error('late buffer failure'))
  49. assert.equal(source.entities.values[0].show,true)
  50. assert.equal(states.at(-1).failed,1)
  51. adapter.destroy()
  52. assert.ok(added.every(model=>model.destroyed))
  53. console.log('PASS real adapter readiness, shared URL, stable metadata, selective update, late error and cleanup')
  54. const guide = { id:'pipe:guide', show:true, polyline:{} }
  55. source.entities.values=[guide]
  56. adapter=createCesiumPipeModels(viewer,{source,base:'/',viewMode:'pipeline'})
  57. const guideNetwork={pipes:[pipe('guide',110.451)],components:[]}
  58. await adapter.sync(guideNetwork)
  59. added.at(-1).readyEvent.raiseEvent()
  60. assert.equal(guide.polyline.distanceDisplayCondition.near,600)
  61. guide.polyline={}
  62. await adapter.sync({pipes:[{...guideNetwork.pipes[0],name:'metadata-only'}],components:[]})
  63. assert.equal(guide.polyline.distanceDisplayCondition?.near,600,'replaced polyline must retain near-model/far-guide rule')
  64. adapter.destroy()
  65. console.log('PASS replacement guide graphics receive distance restriction again')
  66. const terrainStates=[]
  67. globalThis.t32TerrainFails=true
  68. source.entities.values=[{id:'pipe:terrain',show:true}]
  69. adapter=createCesiumPipeModels(viewer,{source,onChange:state=>terrainStates.push(state),base:'/'})
  70. await adapter.sync({pipes:[pipe('terrain',110.431)],components:[]})
  71. assert.equal(terrainStates.at(-1).terrainFailed,true)
  72. assert.equal(terrainStates.at(-1).fallback,1)
  73. assert.equal(source.entities.values[0].show,true)
  74. adapter.destroy()
  75. console.log('PASS terrain failure preserves fallback and publishes reason')
  76. globalThis.t32TerrainFails=false
  77. missing=true
  78. const assetStates=[]
  79. source.entities.values=[{id:'component:404',show:true}]
  80. adapter=createCesiumPipeModels(viewer,{source,onChange:state=>assetStates.push(state),base:'missing-base/'})
  81. await adapter.sync({pipes:[],components:[{code:'404',kind:'valve',industry:'GAS',position:[110.431,28.44]}]})
  82. assert.equal(assetStates.at(-1).failed,1)
  83. assert.equal(assetStates.at(-1).fallback,1)
  84. assert.equal(source.entities.values[0].show,true)
  85. adapter.destroy()
  86. globalThis.fetch=originalFetch
  87. console.log('PASS template 404 reaches real adapter failure path and keeps symbol')