| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- /**
- * T42 关联监测设备临时图层 纯逻辑检查(票 42;规格 14 / 决策票 05)。
- *
- * 零依赖 node 断言脚本(约定见 t01-panel-catalog.mjs)。
- * 运行:node .scratch/cockpit-shell-panel-layout/verify/t42-device-layer.mjs
- *
- * 覆盖验收第 9 条点名的三件事:**集合分类**、**快照生命周期**、**计数互斥**;
- * 外加坐标准入只认后端、设备身份不冒充监测点、模式可见性、摘要口径不被污染等静态守卫。
- */
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import {
- DEVICE_ATTENTION_STATUSES,
- DEVICE_LAYER_KIND,
- DEVICE_PLACEMENT,
- DEVICE_PLACEMENT_TEXT,
- LAYER_MODE,
- associatedLegend,
- classifyDevice,
- countPlacements,
- crossEndpointNotice,
- isVisibleInMode,
- planAssociatedDevices,
- uniqueCodes,
- } from '../../../src/modules/cockpit/domain/deviceLayer.js'
- const checks = []
- function check(name, pass, detail = '') {
- checks.push({ name, pass, detail })
- }
- /** 设备卡片工厂:默认「有有效坐标」,便于逐条只改一个维度。 */
- function card(overrides = {}) {
- return {
- code: 'EQ-1',
- name: '设备一',
- typeName: '压力计',
- status: 'NORMAL',
- statusLabel: '正常',
- statusReasons: [],
- longitude: 110.4,
- latitude: 28.44,
- coordinate: { state: 'VALID', reason: '', crs: 'WGS84', renderable: true, validated: true },
- ...overrides,
- }
- }
- // ================================================================ A 集合分类(每台恰好一类)
- {
- const drawn = classifyDevice({ card: card(), inSnapshot: true, detailOwned: false, mode: LAYER_MODE.OVERVIEW })
- check('A1-valid-coord-is-drawn', drawn === DEVICE_PLACEMENT.DRAWN, `有效坐标 → ${drawn}`)
- // 后端判不可上图:**只认 `renderable`**,前端不重算经纬度。
- const blocked = classifyDevice({
- card: card({ coordinate: { state: 'UNVERIFIED', reason: 'UNVERIFIED_COORDINATE', renderable: false, validated: true } }),
- inSnapshot: true,
- detailOwned: false,
- mode: LAYER_MODE.OVERVIEW,
- })
- check('A2-unverified-goes-no-location', blocked === DEVICE_PLACEMENT.NO_LOCATION,
- `UNVERIFIED/renderable=false → ${blocked}(现网正式边界:清单-only)`)
- // 坐标准入通过但经纬度缺失 → 数据不可用(**不是**无地图位置,两者原因不同)。
- const noCoord = classifyDevice({
- card: card({ longitude: null, latitude: null }),
- inSnapshot: true,
- detailOwned: false,
- mode: LAYER_MODE.OVERVIEW,
- })
- check('A3-missing-numbers-go-unavailable', noCoord === DEVICE_PLACEMENT.UNAVAILABLE,
- `renderable=true 但经纬度缺失 → ${noCoord}(与"后端判无位置"分开)`)
- // 详情还没回来 → 待确认(不算上图、也不算无位置)。
- const pending = classifyDevice({ card: null, inSnapshot: true, detailOwned: false, mode: LAYER_MODE.OVERVIEW })
- check('A4-no-card-is-pending', pending === DEVICE_PLACEMENT.PENDING, `详情未到 → ${pending}`)
- // 详情独有(不在当前快照清单里)→ 不一致,禁止上图。
- const inconsistent = classifyDevice({ card: card(), inSnapshot: false, detailOwned: true, mode: LAYER_MODE.OVERVIEW })
- check('A5-detail-only-is-inconsistent', inconsistent === DEVICE_PLACEMENT.INCONSISTENT,
- `详情独有 → ${inconsistent}(禁止上图)`)
- // 基础设施隐藏;预警处置只留报警/预警。
- check('A6-infrastructure-hides', !isVisibleInMode(LAYER_MODE.INFRASTRUCTURE, 'ALARM'),
- '基础设施模式整体隐藏关联设备图层')
- check('A7-warning-response-only-attention',
- isVisibleInMode(LAYER_MODE.WARNING_RESPONSE, 'ALARM') &&
- isVisibleInMode(LAYER_MODE.WARNING_RESPONSE, 'WARNING') &&
- !isVisibleInMode(LAYER_MODE.WARNING_RESPONSE, 'NORMAL') &&
- !isVisibleInMode(LAYER_MODE.WARNING_RESPONSE, 'OFFLINE') &&
- !isVisibleInMode(LAYER_MODE.WARNING_RESPONSE, 'WORK_ORDER'),
- `预警处置只显示 ${DEVICE_ATTENTION_STATUSES.join('/')}(正常/离线/工单都不上图)`)
- // 状态未知**不猜成正常**、也不因为未知而在总览被隐藏。
- check('A8-unknown-status-still-shown-in-overview', isVisibleInMode(LAYER_MODE.OVERVIEW, ''),
- '总体概览/运行监测不凭状态隐藏(状态未知也显示,视觉归票 44)')
- const unknownHidden = classifyDevice({
- card: card({ status: '' }),
- inSnapshot: true,
- detailOwned: false,
- mode: LAYER_MODE.WARNING_RESPONSE,
- })
- check('A9-unknown-hidden-in-warning-response', unknownHidden === DEVICE_PLACEMENT.HIDDEN,
- `预警处置里状态未知 → ${unknownHidden}(不猜成报警)`)
- // 六类归类各有中文说法(界面不另写措辞)。
- check('A10-placement-text-complete',
- Object.values(DEVICE_PLACEMENT).every((key) => !!DEVICE_PLACEMENT_TEXT[key]),
- `归类文案齐备:${Object.values(DEVICE_PLACEMENT_TEXT).join(' / ')}`)
- }
- // ================================================================ B 快照清单 × 详情(差异如实并列)
- {
- // 规格点名的算例:快照 `[A,B]`、详情 `[B,C]`。
- const plan = planAssociatedDevices({
- snapshotCodes: ['A', 'B'],
- cards: [card({ code: 'B', name: '乙设备' }), card({ code: 'C', name: '丙设备' })],
- mode: LAYER_MODE.OVERVIEW,
- pointCode: 'MP-1',
- })
- check('B1-total-comes-from-snapshot', plan.total === 2,
- `总数=当前快照去重 deviceCodes(${plan.total}),不含详情独有`)
- // **总数 ≠ 清单行数**:清单会多出「详情独有」的行,拿行数当总数会虚报关联设备数。
- // (本票真机走查抓到过这个 UI 口径错误:总数显示 2 而快照只有 1 台。)
- check('B1b-total-is-not-row-count', plan.rows.length === 3 && plan.total === 2,
- `清单 ${plan.rows.length} 行 ≠ 总数 ${plan.total}(详情独有那行不计入总数)`)
- check('B2-snapshot-only-is-pending',
- plan.rows.some((row) => row.code === 'A' && row.placement === DEVICE_PLACEMENT.PENDING),
- 'A 以编码占位并标「设备详情待确认」')
- check('B3-detail-only-is-inconsistent',
- plan.rows.some((row) => row.code === 'C' && row.placement === DEVICE_PLACEMENT.INCONSISTENT),
- 'C 保留卡片但标「与当前快照不一致」且禁止上图')
- check('B4-all-codes-kept', plan.rows.length === 3 && ['A', 'B', 'C'].every((code) => plan.rows.some((r) => r.code === code)),
- `两边都不静默丢弃:${plan.rows.map((r) => r.code).join(',')}`)
- check('B5-only-snapshot-drawn',
- plan.rows.filter((row) => row.placement === DEVICE_PLACEMENT.DRAWN).map((row) => row.code).join(',') === 'B',
- '只有同时属于快照清单且坐标准入有效的设备进入显示候选')
- const notice = crossEndpointNotice(plan.rows)
- check('B6-cross-endpoint-notice', notice.risky && notice.text.includes('不同版'),
- `跨端点风险如实提示:「${notice.text}」`)
- // 完全一致时不该无端报警。
- const clean = planAssociatedDevices({
- snapshotCodes: ['A'],
- cards: [card({ code: 'A' })],
- mode: LAYER_MODE.OVERVIEW,
- pointCode: 'MP-1',
- })
- check('B7-no-notice-when-consistent', crossEndpointNotice(clean.rows).risky === false,
- '快照与详情一致时不弹风险提示(避免恒真提示)')
- // 去重:后端源顺序去重未排序,前端不重排。
- check('B8-dedupe-keeps-order',
- JSON.stringify(uniqueCodes(['B', 'A', 'B', '', ' ', 'C', 'A'])) === JSON.stringify(['B', 'A', 'C']),
- `去重保序:${uniqueCodes(['B', 'A', 'B', '', ' ', 'C', 'A']).join(',')}`)
- // 清单尚未加载(快照里还没有这个点位)→ 全空且如实可辨。
- const notLoaded = planAssociatedDevices({ snapshotCodes: [], cards: [], mode: LAYER_MODE.OVERVIEW, pointCode: 'MP-X' })
- check('B9-empty-snapshot-yields-no-rows', notLoaded.total === 0 && notLoaded.rows.length === 0,
- '点位不在当前快照时总数为 0(调用方据此显示"待当前快照核对",不猜总数)')
- }
- // ================================================================ C 计数互斥(各类之和可解释总数)
- {
- // 计数互斥的算例**刻意只用"数据可用"的设备**:把优先级判定留给 C6/C7 专门测,
- // 两条断言各管一件事(否则退回模式优先后先红的是 C3,C6 的判据就形同虚设 —— 变异 M12 实测)。
- const plan = planAssociatedDevices({
- snapshotCodes: ['A', 'C', 'D', 'F'],
- cards: [
- card({ code: 'A', status: 'ALARM' }), // 报警 → 预警处置里可上图
- card({ code: 'C', status: 'NORMAL' }), // 数据没问题,只是本模式不显示
- // D 无卡片 → 待确认
- card({ code: 'F', status: 'WARNING' }), // 预警 → 也可上图
- ],
- mode: LAYER_MODE.WARNING_RESPONSE,
- pointCode: 'MP-1',
- })
- const counts = plan.counts
- const sum = Object.values(counts).reduce((total, value) => total + value, 0)
- check('C1-counts-are-mutually-exclusive', sum === plan.rows.length,
- `六类之和 ${sum} == 行数 ${plan.rows.length}(每台恰好一类)`)
- check('C2-drawn-count-matches-rows',
- counts[DEVICE_PLACEMENT.DRAWN] === 2 && counts[DEVICE_PLACEMENT.DRAWN] === plan.rows.filter((r) => r.placement === DEVICE_PLACEMENT.DRAWN).length,
- `已上图计数与行一致:${counts[DEVICE_PLACEMENT.DRAWN]}(报警 + 预警两台)`)
- check('C3-each-reason-has-a-row',
- counts[DEVICE_PLACEMENT.HIDDEN] === 1 && counts[DEVICE_PLACEMENT.PENDING] === 1,
- `差额各有实例可解释:${JSON.stringify(counts)}`)
- // **数据可用性先于模式可见性**(本票实现时抓到并修掉的真问题)。
- // 算例必须是「模式**会**隐藏它、且数据也有问题」的组合 —— 否则两种顺序结果相同、判据形同虚设:
- // · `NORMAL` + `renderable:false`:先判模式 → HIDDEN(谎话:切模式也看不到它);
- // 先判数据 → NO_LOCATION(实话:它任何模式都上不了图)。
- // (早先用 `ALARM` 当算例是错的:ALARM 在预警处置里可见,两种顺序都落 NO_LOCATION,
- // 变异 M12 因此测不出来 —— 这正是变异测试的价值。)
- const blockedInWarningMode = classifyDevice({
- card: card({ status: 'NORMAL', coordinate: { state: 'UNVERIFIED', reason: 'UNVERIFIED_COORDINATE', renderable: false, validated: true } }),
- inSnapshot: true,
- detailOwned: false,
- mode: LAYER_MODE.WARNING_RESPONSE,
- })
- check('C6-data-availability-beats-mode-visibility',
- blockedInWarningMode === DEVICE_PLACEMENT.NO_LOCATION,
- `预警处置里正常但坐标被否决 → ${blockedInWarningMode}(**不是**"模式隐藏":切模式也看不到它)`)
- // 反过来:数据好的设备在预警处置里因为状态被隐藏,这才是真的"模式隐藏"。
- const normalInWarningMode = classifyDevice({
- card: card({ status: 'NORMAL' }),
- inSnapshot: true,
- detailOwned: false,
- mode: LAYER_MODE.WARNING_RESPONSE,
- })
- check('C7-good-data-hidden-by-mode',
- normalInWarningMode === DEVICE_PLACEMENT.HIDDEN,
- `预警处置里正常设备 → ${normalInWarningMode}(数据没问题,只是本模式不显示)`)
- // 独立图例:只统计本图层,且**合计 == 行数**(与摘要图例互不计入)。
- //
- // 行数**不写死**:分类是跟着规格长的(票 44 追加了 `terrain-pending`),
- // 钉死 6 行会让"分类变多"这件事在闸门里表现为假红(本票实测)。
- // 真正要守的判据是"图例与计数同源且完整",所以判"行数 == 分类数"。
- const legend = associatedLegend(counts)
- const legendSum = legend.reduce((total, row) => total + row.count, 0)
- const placementKinds = Object.keys(DEVICE_PLACEMENT).length
- check('C4-legend-independent-and-complete',
- legendSum === plan.rows.length && legend.length === placementKinds,
- `设备图例合计 ${legendSum} == 行数 ${plan.rows.length};图例 ${legend.length} 行 == 分类 ${placementKinds} 类(独立于摘要图例)`)
- // 空输入不炸、全 0。
- const empty = countPlacements([])
- check('C5-empty-counts-zero', Object.values(empty).every((value) => value === 0), '空清单各类为 0')
- // 票 44:**坐标有效但地形未就绪** → 单列 `terrain-pending`(规格明写要与坐标无效分开)。
- const terrainPending = classifyDevice({
- card: card(),
- inSnapshot: true,
- detailOwned: false,
- mode: LAYER_MODE.OVERVIEW,
- terrainReady: false,
- })
- check('C8-terrain-pending-is-its-own-class',
- terrainPending === DEVICE_PLACEMENT.TERRAIN_PENDING,
- `坐标有效 + 地形未就绪 → ${terrainPending}(不是 no-location、也不算已上图)`)
- // 对照组:地形就绪时同一台设备正常可上图(证明是地形那一条在起作用)。
- const terrainReady = classifyDevice({
- card: card(),
- inSnapshot: true,
- detailOwned: false,
- mode: LAYER_MODE.OVERVIEW,
- terrainReady: true,
- })
- check('C9-terrain-ready-same-device-draws',
- terrainReady === DEVICE_PLACEMENT.DRAWN,
- `同一台设备地形就绪 → ${terrainReady}(对照组,证明判据是地形而非数据)`)
- }
- // ================================================================ D 静态守卫(口径不被污染)
- {
- const root = new URL('../../../src/modules/cockpit/', import.meta.url)
- const read = (rel) => readFileSync(new URL(rel, root), 'utf8')
- const engine = read('services/cesiumEngine.js')
- const mapService = read('services/mapService.js')
- const store = read('stores/deviceLayerStore.js')
- const selection = read('pointSelection.js')
- const shell = read('CockpitShell.vue')
- // ① 独立数据源:设备图层与摘要标记层必须是两个数据源(结构性保证不写回摘要)。
- check('D1-separate-data-source',
- /export const DEVICE_MARKER_SOURCE = 'cockpit-associated-device-markers'/.test(engine) &&
- /export const POINT_MARKER_SOURCE = 'cockpit-point-markers'/.test(engine) &&
- /DEVICE_MARKER_SOURCE/.test(mapService),
- '关联设备有自己的数据源,与摘要点位标记分开')
- // ② 设备身份同时带 deviceCode 与 monitoringPointCode,**绝不写 pointCode**(不冒充监测点)。
- const syncBody = (/export function syncAssociatedDevices[\s\S]*?\n\}/.exec(engine) || [''])[0]
- check('D2-device-identity-not-point-identity',
- /deviceCode: device\.code/.test(syncBody) && /monitoringPointCode: device\.pointCode/.test(syncBody) &&
- !/\bpointCode:/.test(syncBody.replace(/monitoringPointCode/g, '')),
- '设备实体带 deviceCode + monitoringPointCode,且不写 pointCode(不冒充监测点)')
- // ③ 设备图层不写摘要计数(`data-map-markers*` 只由摘要路径写)。
- // 按**函数体**切片:设备图层的读数走 `data-map-devices*`,与摘要的 `data-map-markers*` 分开。
- const devicePaintBody = (/function paintDeviceState\(\)[\s\S]*?\n\}/.exec(mapService) || [''])[0]
- check('D3-device-layer-does-not-touch-summary-counters',
- /dataset\.mapDevices\b/.test(devicePaintBody) && !/mapMarkers/.test(devicePaintBody),
- '设备图层只写 data-map-devices*,不碰摘要的 data-map-markers*')
- // ④ 坐标准入只认后端 renderable(不重算经纬度范围)。
- const domain = read('domain/deviceLayer.js')
- check('D4-admission-only-backend-renderable',
- /isRenderable\(card\)/.test(domain) && !/longitude\s*[<>]=?\s*-?1[78]0/.test(domain),
- '准入只认 isRenderable(后端结论),前端不另算经纬度范围')
- // ⑤ 绝不复制监测点坐标:设备行只从卡片取坐标。
- check('D5-never-copies-point-coordinates',
- /longitude: card \? card\.longitude : null/.test(domain) &&
- !/longitude:\s*object\.longitude|point\.longitude/.test(domain),
- '设备坐标只来自设备卡片,绝不借用点位坐标或人工偏移')
- // ⑥ 快照变化先撤旧图层(规格明写)。
- // 守卫必须**只截 `invalidateForSnapshot` 自己的函数体**:整个文件里 `clearAssociatedDevices()`
- // 还有别的调用点(`syncForSelection` 的空选择分支、`reset`),对全文匹配会被它们满足
- // —— 变异 M16 实测:拿掉这一处仍全绿。这是本仓第四次栽在"守卫比它要守的东西宽"上。
- const invalidateBody = (/function invalidateForSnapshot\(\)[\s\S]*?\n \}/.exec(store) || [''])[0]
- check('D6-snapshot-change-clears-first',
- /clearAssociatedDevices\(\)/.test(invalidateBody) &&
- invalidateBody.indexOf('clearAssociatedDevices()') < invalidateBody.indexOf('syncForSelection') &&
- /syncDeviceLayerForSnapshot/.test(shell),
- '快照变化时**先**撤下旧图层再等新的(撤下在重算之前;外壳确实调了它)')
- // ⑦ 同快照翻页不重建:计划指纹去重。
- check('D7-same-plan-not-repainted',
- /lastPlanKey/.test(store) && /key === lastPlanKey/.test(store),
- '同一份计划(同点位同清单)不重画 ⇒ 翻页不闪烁')
- // ⑧ 统一入口:地图与台账都走 selectMonitoringPoint,且它启动设备图层。
- check('D8-single-entry-starts-device-layer',
- /deviceLayerStore\.syncForSelection/.test(selection),
- '统一选择入口启动设备图层(地图与台账因此行为一致)')
- // ⑨ 卸载时拆监听并复位(票 15/37 的"漏取消"坑)。
- check('D9-teardown',
- /for \(const stop of stopDeviceWatches\) stop\(\)/.test(shell) && /deviceLayerStore\.reset\(\)/.test(shell),
- '卸载时拆掉四个监听并复位设备图层')
- // ⑩ 设备类型键与摘要对象并列而不相同。
- check('D10-device-kind-distinct', DEVICE_LAYER_KIND === 'monitoring-device' && !/DEVICE_LAYER_KIND\s*=\s*'monitoring-point'/.test(domain),
- `设备类型键 ${DEVICE_LAYER_KIND} 与 monitoring-point 并列而不相同`)
- }
- const failed = checks.filter((item) => !item.pass)
- console.log('')
- for (const item of checks) {
- if (!item.pass) console.log(` ✗ ${item.name}${item.detail ? ` — ${item.detail}` : ''}`)
- }
- console.log(`T42 关联设备图层检查:${checks.length - failed.length}/${checks.length} 项通过。`)
- if (failed.length) process.exit(1)
- console.log('T42 关联设备图层全部通过。')
|