anchor-migration-verify.mjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. // 锚点迁移对抗式验证(验收清单第 2 项):
  2. // 1. 新断言(verticalOrigin=BOTTOM + 尺寸随母版比例)在「未迁移的旧实现」上必须失败;
  3. // 2. 旧断言(CENTER + 方形 18×18)在「已迁移的新实现」上也必须失败;
  4. // 两向都能失败 → 迁移是真实的,不是「改完还是绿的」。
  5. import { readFileSync } from 'node:fs'
  6. import vm from 'node:vm'
  7. // —— 加载引擎模块的精简副本(跳过 cesium import;用 stub 顶住) ——
  8. const src = readFileSync('src/modules/cockpit/services/cesiumEngine.js', 'utf8')
  9. // 砍掉静态 import(stub 由沙箱注入)
  10. .replace(/^import[\s\S]*?from\s+['"][^'"]+['"];?\s*$/gm, '')
  11. // 砍掉 Cesium 常量解构(沙箱注入)
  12. .replace(/^const\s*\{[\s\S]*?\}\s*=\s*Cesium;?/m, '')
  13. function loadEngine() {
  14. const created = []
  15. const ctx = vm.createContext({
  16. // Cesium stub(含 BOTTOM,供新锚点断言)
  17. VerticalOrigin: { CENTER: 'CENTER', BOTTOM: 'BOTTOM' },
  18. HeightReference: { CLAMP_TO_GROUND: 'CLAMP_TO_GROUND' },
  19. Cartesian2: class { constructor(x, y) { this.x = x; this.y = y } },
  20. Cartesian3: { fromDegrees: (a, b) => ({ lon: a, lat: b }) },
  21. Color: { WHITE: { withAlpha: (a) => ({ alpha: a }) }, BLACK: { withAlpha: (a) => ({ alpha: a }) } },
  22. LabelStyle: { FILL_AND_OUTLINE: 0 },
  23. DistanceDisplayCondition: class {},
  24. // 引擎模块引用的其他域模块(按需最小 stub;本验证只走 syncPointMarkers)
  25. EMPHASIS_STYLE: {}, shouldEmphasize: () => false,
  26. MAP_LABEL_STYLE: {}, mapLabelText: () => ({ text: '', font: '', pixelOffset: {} }),
  27. createTilesetLoadingController: () => ({}), tilesetOptions: {},
  28. createRegionLoadingController: () => ({}), contentAdmission: {}, removeSceneTileset: () => {},
  29. DEVICE_EFFECT: {}, RING_LAYERS: 0, blinkStrengthAt: () => 0, glowScaleAt: () => 0,
  30. glowStrengthAt: () => 0, hasGlow: () => false, pulseStrengthAt: () => 0,
  31. ringEntityCount: () => 0, ringFrameAt: () => 0, spinAngleAt: () => 0,
  32. GRADE_DEFAULTS: {}, parseHexColor: () => null,
  33. planToDataset: (plan) => ({ markerMaster: plan.master, markerInner: plan.inner.enabled ? plan.inner.icon : '', markerAnchor: `${plan.anchor.x},${plan.anchor.y}`, markerDesaturate: plan.desaturate ? 'yes' : 'no' }),
  34. renderMarker: async () => 'data:,',
  35. // 引擎内部可能引用的 DOM/全局
  36. console, setTimeout, clearTimeout, Math, JSON, Number, Object, Array, Map, Set, WeakMap, Promise,
  37. document: undefined,
  38. __created: created,
  39. pointMarkerSource: () => ({ entities: { removeAll() {}, remove() {}, add(e) { created.push(e); return e }, values: [] } }),
  40. deviceMarkerSource: () => ({ entities: { removeAll() {}, add(e) { created.push(e); return e }, values: [] } }),
  41. deviceBaseStyle: { clear() {}, set() {}, get() { return null } },
  42. stopDeviceEffects() {}, applyDeviceEffects() {}, ringEntities: new Map(), blinkEntities: new Map(), spinEntities: new Map(),
  43. })
  44. try { vm.runInContext(src + '\nglobalThis.__engine = { syncPointMarkers };', ctx) } catch (e) {
  45. console.log('(引擎加载跳过部分依赖错误,尝试仅提取 syncPointMarkers)', e.message.slice(0, 80))
  46. // 提取函数体 + 其紧邻的模块级依赖(markerEntityIndex / markerFingerprint)单独执行
  47. const idxDecl = /const markerEntityIndex = new WeakMap\(\)[^\n]*/.exec(src)[0]
  48. const fpFn = /function markerFingerprint\(marker\)[\s\S]*?\n\}/.exec(src)[0]
  49. const fnMatch = src.match(/export function syncPointMarkers[\s\S]*?\n\}/)
  50. vm.runInContext(
  51. idxDecl + '\n' + fpFn + '\n' +
  52. 'globalThis.__engine = { syncPointMarkers: ' + fnMatch[0].replace('export function syncPointMarkers', 'function syncPointMarkers') + ' };',
  53. ctx,
  54. )
  55. }
  56. return { syncPointMarkers: ctx.__engine.syncPointMarkers, created }
  57. }
  58. const marker = {
  59. key: 'MP-001|monitoring-point', kind: 'monitoring-point', code: 'MP-001', name: '',
  60. deviceCodes: [], status: 'ALARM', visualKey: 'ALARM', tone: 'lv1', shape: 'triangle',
  61. label: '报警', longitude: 110.4, latitude: 28.4,
  62. }
  63. const plan = {
  64. master: 'solid',
  65. inner: { icon: '', enabled: false, diameterRatio: 0.6, center: { x: 0.499, y: 0.397 } },
  66. size: { width: 27, height: 33.93 },
  67. anchor: { x: 0.4987, y: 1.0 },
  68. tint: 'lv1', desaturate: false,
  69. labelOffsetY: -39.93, labelMaxWidth: 43.2,
  70. draw: { width: 86.06, height: 92.99, pad: 2.86 },
  71. }
  72. let pass = 0
  73. let fail = 0
  74. const check = (cond, label) => { if (cond) { pass++; console.log(` ✓ ${label}`) } else { fail++; console.log(` ✗ ${label}`) } }
  75. console.log('① 新断言 × 新实现(传 planOf):BOTTOM 锚定 + 母版比例尺寸')
  76. {
  77. const { syncPointMarkers, created } = loadEngine()
  78. syncPointMarkers({}, [marker], () => '#ff3b52', () => 'data:,', () => plan, () => '#ff3b52')
  79. const bb = created[0].billboard
  80. check(bb.verticalOrigin === 'BOTTOM', `verticalOrigin = BOTTOM(实际 ${bb.verticalOrigin})`)
  81. check(bb.width === 86.06 && bb.height === 92.99, `尺寸 = draw 86.06×92.99(实际 ${bb.width}×${bb.height})`)
  82. check(created[0].properties.markerMaster && created[0].properties.markerMaster === 'solid', '宿主读数含 markerMaster=solid')
  83. }
  84. console.log('② 新断言 × 旧实现(不传 planOf):必须仍 CENTER/18×18 —— 新断言对旧实现失败即「能查到未迁移」')
  85. {
  86. const { syncPointMarkers, created } = loadEngine()
  87. syncPointMarkers({}, [marker], () => '#ff3b52', () => 'data:,')
  88. const bb = created[0].billboard
  89. check(bb.verticalOrigin === 'CENTER', `旧实现保持 CENTER(实际 ${bb.verticalOrigin})—— 新断言若在此判 BOTTOM 会假红`)
  90. check(!(bb.width === 27 && bb.height === 33.93), `旧实现尺寸非 18×22.63(实际 ${bb.width}×${bb.height})—— 新断言在此失败是预期`)
  91. }
  92. console.log('③ 旧断言 × 新实现:CENTER/18×18 的旧断言对新实现必须失败(防「改完仍是绿的」)')
  93. {
  94. const { syncPointMarkers, created } = loadEngine()
  95. syncPointMarkers({}, [marker], () => '#ff3b52', () => 'data:,', () => plan, () => '#ff3b52')
  96. const bb = created[0].billboard
  97. check(bb.verticalOrigin !== 'CENTER', `新实现 verticalOrigin ≠ CENTER(实际 ${bb.verticalOrigin})→ 旧 CENTER 断言会失败`)
  98. check(!(bb.width === 18 && bb.height === 18), `新实现尺寸 ≠ 18×18(实际 ${bb.width}×${bb.height})→ 旧方形断言会失败`)
  99. }
  100. console.log(`\n对抗式验证:${pass} 过 / ${fail} 败`)
  101. if (fail) process.exitCode = 1