triage-overlap-probe.mjs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. // Triage-only probe: executes production functions with Cesium API doubles.
  2. // No application changes, browser, server, or GPU rendering. Expected red until #47 is fixed.
  3. import assert from 'node:assert/strict'
  4. import { readFileSync } from 'node:fs'
  5. import vm from 'node:vm'
  6. import { groupOverlappingDevices, fanLayout } from '../../../../../src/modules/cockpit/domain/overlapDevices.js'
  7. import * as effects from '../../../../../src/modules/cockpit/domain/deviceEffects.js'
  8. const engine = readFileSync(new URL('../../../../../src/modules/cockpit/services/cesiumEngine.js', import.meta.url), 'utf8')
  9. const productionFunction = (name) => {
  10. const match = engine.match(new RegExp(`(?:export )?function ${name}\\([^]*?\\n\\}`))
  11. assert.ok(match, `${name} exists`)
  12. return match[0].replace(/^export /, '')
  13. }
  14. const property = (initial) => {
  15. let value = initial
  16. return { getValue: () => value, setValue: (next) => { value = next } }
  17. }
  18. const rows = ['ALARM', 'NORMAL'].map((status, index) => ({
  19. code: index ? 'EQ-N' : 'EQ-A', pointCode: 'MP-T47', status,
  20. longitude: 110.3632, latitude: 28.4524, placement: 'drawn',
  21. coordinate: { renderable: true }, size: 14, shape: index ? 'circle' : 'triangle',
  22. }))
  23. const group = groupOverlappingDevices(rows)[0]
  24. const plan = effects.planDeviceEffects({ devices: rows, enabled: true })
  25. const source = { entities: {
  26. values: [],
  27. add(input) {
  28. const entity = { show: true, ...input }
  29. entity.properties = Object.fromEntries(Object.entries(input.properties || {}).map(([key, value]) => [key, property(value)]))
  30. this.values.push(entity)
  31. return entity
  32. },
  33. remove(entity) { this.values = this.values.filter((item) => item !== entity) },
  34. removeAll() { this.values = [] },
  35. } }
  36. let requests = 0
  37. const viewer = { scene: { requestRender() { requests += 1 } } }
  38. const context = vm.createContext({
  39. ...effects,
  40. source, viewer, rows, groups: [group], plan,
  41. deviceSources: new Map([[viewer, source]]),
  42. deviceMarkerSource: () => source,
  43. deviceBaseStyle: new Map(),
  44. deviceEffectState: { viewer, plan, selectedCode: '', tick: 0, raf: 0 },
  45. RING_ENTITY_PREFIX: 'fx|',
  46. Color: { WHITE: { withAlpha: (alpha) => ({ alpha }) } },
  47. Cartesian2: class { constructor(x, y) { this.x = x; this.y = y } },
  48. Cartesian3: { fromDegrees: (longitude, latitude) => ({ longitude, latitude }) },
  49. HeightReference: { CLAMP_TO_GROUND: 'ground' }, VerticalOrigin: { CENTER: 'center' },
  50. mapTextLabel: () => ({ show: true }),
  51. stopDeviceEffects: () => {}, ensureEffectLoop: () => {},
  52. Date: { now: () => 300 },
  53. })
  54. for (const name of ['syncAssociatedDevices', 'setOverlapPresentation', 'ringEntitiesOf', 'ensureRingEntities', 'clearRingEntities', 'baseStyleOf', 'animatedByCode', 'writeEffectFrame', 'applyDeviceEffects', 'readDeviceEffects']) {
  55. vm.runInContext(productionFunction(name), context)
  56. }
  57. vm.runInContext(`syncAssociatedDevices(viewer, rows, () => 'red', () => 'icon', groups);
  58. applyDeviceEffects(viewer, plan, { devices: rows, fxImageOf: () => 'halo' });`, context)
  59. let failures = 0
  60. function snapshot(name, input) {
  61. context.input = input
  62. vm.runInContext('setOverlapPresentation(viewer, input); writeEffectFrame()', context)
  63. const visibleFx = source.entities.values.filter((entity) => entity.properties.effectFx?.getValue() === true && entity.show !== false && entity.billboard.color.alpha > 0)
  64. const visibleDevices = source.entities.values.filter((entity) => entity.properties.deviceCode && entity.show !== false).map((entity) => entity.properties.deviceCode.getValue())
  65. const readback = vm.runInContext('readDeviceEffects(viewer)', context)
  66. console.log(JSON.stringify({ case: name, animatedPlan: plan.animated.length, abnormalCount: plan.abnormalCount, degraded: plan.degraded, visibleDevices, visibleFx: visibleFx.length, reportedFx: readback.fx.length, fxReadbackContainsVisibility: readback.fx.every((item) => 'visible' in item || 'show' in item), requests }))
  67. return visibleFx.length
  68. }
  69. function expectVisible(name, input) {
  70. const count = snapshot(name, input)
  71. try { assert.ok(count > 0, 'expected visible abnormal effect while associated layer and effects are enabled'); console.log(`PASS ${name}`) }
  72. catch (error) { failures += 1; console.log(`FAIL ${name}: ${error.message}`) }
  73. }
  74. expectVisible('collapsed', { groupKey: group.key, state: 'collapsed' })
  75. const offsets = Object.fromEntries(fanLayout(group).map((item) => [item.deviceCode, { x: item.x, y: item.y }]))
  76. expectVisible('expanded-control', { groupKey: group.key, state: 'expanded', offsets })
  77. expectVisible('selected-NORMAL', { groupKey: group.key, state: 'selected', selectedCode: 'EQ-N' })
  78. expectVisible('selected-ALARM-control', { groupKey: group.key, state: 'selected', selectedCode: 'EQ-A' })
  79. console.log(`T47 triage probe: ${failures} failed expectations; render visibility logic only, not a browser/GPU verification.`)
  80. process.exitCode = failures ? 1 : 0