/** * T20 总体概览模式面板 纯逻辑检查(票 20)。 * * 零依赖 node 断言脚本(仓库没配测试运行器,约定见 t01-panel-catalog.mjs)。 * 运行:node .scratch/cockpit-shell-panel-layout/verify/t20-overview-panels.mjs * * 六段: * A 面板映射与注册:四块与 ADR 0003 目录一致、不越界接其它模式、注册表覆盖四个模式共 16 块 * B 契约适配:三份载荷翻领域对象、BigDecimal 字符串、缺失记账、工单状态 0–7 全铺 * C 视图层:验收点名的画面元素逐项在场;分母为 0 显示占位符而不是 0.0%;缺口如实说明 * D 分区独立:三块各自成败、一块坏不遮其它两块 + 对抗式反证;态势总览由三块合成 * E 联动:指标点击 → 切模式;行业行 → 写全局行业筛选(都走 store 的唯一入口) * F 静态守卫:domain 纯函数、渲染层不认识 panelId、不拼路径、外壳走同一接缝 * * 契约依据:后端 `CockpitOverview{Infrastructure,Monitoring,WarningSettings}Response` * 与三个 Service(`E:\pipe-ner-server\...\cockpit\overview\`)。 */ import { readFileSync } from 'node:fs' import { createPinia, setActivePinia } from 'pinia' import { columnsForMode, panelsForMode } from '../../../src/modules/cockpit/domain/panelCatalog.js' import { OVERVIEW_PANEL_IDS, MONITORING_FAULT_GAP, toOverviewInfrastructure, toOverviewMonitoring, toOverviewWarningSettings, toOverviewRegions, situationSummaryPanel, infrastructureSummaryPanel, monitoringSummaryPanel, alertSettingsPanel, OVERVIEW_PANEL_BUILDERS, } from '../../../src/modules/cockpit/domain/overviewPanels.js' import { hasPanelView, implementedPanelIds, panelView } from '../../../src/modules/cockpit/domain/panelViews.js' import { SECTION_STATE, section } from '../../../src/modules/cockpit/domain/sectionState.js' import { DETAIL_PLACEHOLDER } from '../../../src/modules/cockpit/domain/detailFlow.js' import { OVERVIEW_PATHS, OVERVIEW_RANGE } from '../../../src/modules/cockpit/services/overviewApi.js' import { WORK_ORDER_STATES } from '../../../src/modules/cockpit/domain/workOrder.js' import { useOverviewStore } from '../../../src/modules/cockpit/stores/overviewStore.js' import { useUiStore } from '../../../src/modules/cockpit/stores/uiStore.js' import { useFilterStore } from '../../../src/modules/cockpit/stores/filterStore.js' const checks = [] function check(name, pass, evidence = '') { checks.push(pass) console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${evidence ? ` — ${evidence}` : ''}`) } const ready = (data) => ({ ...section(SECTION_STATE.SUCCESS, { data }), loaded: true }) const moduleRoot = new URL('../../../src/modules/cockpit/', import.meta.url) const read = (path) => readFileSync(new URL(path, moduleRoot), 'utf8') // ================================================================ A 面板映射与注册 { check( 'A1-four-overview-panels-match-catalog', panelsForMode('overview').join(',') === 'situationSummary,infrastructureSummary,monitoringSummary,alertSettings', `目录:${panelsForMode('overview').join(' / ')}`, ) const columns = columnsForMode('overview') check( 'A2-columns-are-two-and-two', columns.left.join(',') === 'situationSummary,infrastructureSummary' && columns.right.join(',') === 'monitoringSummary,alertSettings', `左栏 ${columns.left.join('/')} · 右栏 ${columns.right.join('/')}`, ) check( 'A3-builders-cover-exactly-the-four', OVERVIEW_PANEL_IDS.join(',') === 'situationSummary,infrastructureSummary,monitoringSummary,alertSettings' && Object.keys(OVERVIEW_PANEL_BUILDERS).join(',') === OVERVIEW_PANEL_IDS.join(','), `构造器与目录逐项一致(${Object.keys(OVERVIEW_PANEL_BUILDERS).length} 个)`, ) check( 'A4-registry-covers-all-four-modes', OVERVIEW_PANEL_IDS.every(hasPanelView) && ['archive', 'operation', 'management', 'emergency'].every(hasPanelView) && ['monitoringOverview', 'monitoringDistribution', 'operationOverview', 'monitoringPointLedger'].every(hasPanelView) && ['historyAnalysis', 'responseEfficiency', 'eventAnalysis'].every(hasPanelView), `已接面板共 ${implementedPanelIds().length} 块(本票把四个模式都接上了)`, ) check( 'A5-unknown-panel-still-returns-null', panelView('notAPanel', {}, ready({})) === null && hasPanelView('notAPanel') === false, '未注册的 panelId 仍回 null(调用方画占位),注册表不会假装认识它', ) check( 'A6-overview-does-not-claim-other-modes-panels', !OVERVIEW_PANEL_IDS.includes('archive') && !OVERVIEW_PANEL_IDS.includes('currentEvents') && !OVERVIEW_PANEL_IDS.includes('monitoringOverview'), '本票只接总体概览那四块,不越界接其它模式的面板', ) } // ================================================================ B 契约适配 { const infra = toOverviewInfrastructure({ updatedAt: '2026-09-29T10:00:00+08:00', totals: { facilityCount: 5, smartDeviceCount: 9 }, industries: [ { code: 'GAS', key: 'gas', label: '燃气', facilityCount: 2, smartDeviceCount: 4, facilityTypes: [{ code: 'PIPE', name: '燃气管线', count: 2, lengthKm: '12.4', scale: 'medium' }] }, { code: 'DRAINAGE', key: 'drainage', facilityCount: 1, smartDeviceCount: 2 }, ], excluded: { unknownTypeCount: 1, typeKindMismatchCount: 0 }, }) check( 'B1-infrastructure-totals-and-industries', infra.facilityCount === 5 && infra.smartDeviceCount === 9 && infra.industries.length === 2 && infra.excluded.unknownTypeCount === 1, `设施 ${infra.facilityCount} · 智能设备 ${infra.smartDeviceCount} · 行业 ${infra.industries.length}`, ) // BigDecimal 在线上是字符串(`@JsonFormat(shape=STRING)`):必须认字符串,不能变假 0。 check( 'B1b-bigdecimal-string-is-read', infra.industries[0].facilityTypes[0].lengthKm === 12.4 && infra.industries[0].facilityTypes[0].scale === 'medium', `lengthKm "12.4" → ${infra.industries[0].facilityTypes[0].lengthKm}(字符串被认下,不是 0)`, ) check( 'B1c-industry-label-falls-back-to-dictionary', infra.industries[1].label === '排水', `缺 label 的行业按码翻:DRAINAGE → ${infra.industries[1].label}(不显示裸码)`, ) // 本端点**没有** regionDistribution:这是契约现状,不是漏读。 check( 'B1d-overview-infrastructure-has-no-regions', infra.regions === undefined, '`/overview/infrastructure` 不含区域分布(区域走票 17 的 base-archives,见文件头第 ② 条)', ) const monitoring = toOverviewMonitoring({ counts: { deviceCount: 9, monitoringPointCount: 7, monitoringFacilityCount: 3, infrastructureFacilityCount: 5, status: { online: 6, offline: 1, stale: 1, unknownOrMissing: 1, denominator: 8 }, onlineRate: { value: '0.7500', onlineCount: 6, denominator: 8, state: 'CURRENT' }, }, metricFreshness: { status: 'CURRENT', updatedAt: '2026-09-29T09:59:00+08:00' }, formalScope: { industries: ['GAS', 'WATER_SUPPLY'], keys: ['gas', 'water'] }, typeDistribution: [{ industry: { code: 'GAS', label: '燃气' }, deviceType: { code: 'PIPE', name: '燃气管线' }, deviceCount: 4 }], excluded: { unknownTypeCount: 0, typeKindMismatchCount: 0 }, }) check( 'B2-monitoring-counts-and-four-status-buckets', monitoring.deviceCount === 9 && monitoring.onlineRate === 0.75 && monitoring.deviceStatus.online === 6 && monitoring.deviceStatus.offline === 1 && monitoring.deviceStatus.stale === 1 && monitoring.deviceStatus.unknownOrMissing === 1, `四态桶:在线 ${monitoring.deviceStatus.online} / 离线 ${monitoring.deviceStatus.offline} / 滞后 ${monitoring.deviceStatus.stale} / 未知 ${monitoring.deviceStatus.unknownOrMissing}`, ) // 验收第 3 条点名的「故障统计」在契约里**不存在**独立桶 —— 适配层不得凭空造一个。 check( 'B2b-no-fault-bucket-is-invented', monitoring.deviceStatus.fault === undefined && !('fault' in monitoring.deviceStatus), '不发明「故障」桶(后端只有 online/offline/stale/unknownOrMissing 四态)', ) check( 'B2c-metric-freshness-is-kept', monitoring.metricFreshness.updatedAt === '2026-09-29T09:59:00+08:00' && monitoring.metricFreshness.status === 'CURRENT', '逐设备最近采集时间在本端点上(票 18 的 I2/BT-8 缺口在这里有值)', ) check( 'B2d-formal-scope-is-kept', monitoring.formalIndustries.join(',') === 'GAS,WATER_SUPPLY', `正式行业范围回显:${monitoring.formalIndustries.join(',')}`, ) const warnings = toOverviewWarningSettings({ updatedAt: '2026-09-29T10:00:00+08:00', displayName: '预警设置', selectedMode: 'WARNING', range: { type: 'LAST_7_DAYS', start: '2026-09-23T00:00:00+08:00', end: '2026-09-30T00:00:00+08:00' }, warning: { eventFamily: 'WARNING', state: 'READY', total: 8, handled: 5, pending: 2, cancelledCount: 1, unlinkedEventCount: 1, handlingEfficiency: { numerator: 5, denominator: 8, value: '0.6250', state: 'AVAILABLE' }, workOrderStates: { 0: 1, 5: 2 }, trend: [{ bucket: '2026-09-28', total: 3, handled: 2 }], }, alarm: { eventFamily: 'ALARM', state: 'READY', total: 9, handled: 6, pending: 2, cancelledCount: 1, unlinkedEventCount: 0, handlingEfficiency: { numerator: 6, denominator: 9, value: '0.6667', state: 'AVAILABLE' }, workOrderStates: {}, trend: [{ bucket: '2026-09-28', total: 4, handled: 3 }], }, }) check( 'B3-both-families-come-from-one-payload', warnings.warning.total === 8 && warnings.alarm.total === 9 && warnings.warning.family === 'WARNING' && warnings.alarm.family === 'ALARM', `一次取数同时给两族:预警 ${warnings.warning.total} 条 / 报警 ${warnings.alarm.total} 条(故不需要切换)`, ) check( 'B3b-efficiency-carries-state-alongside-value', warnings.warning.efficiency.handlingRate === 0.625 && warnings.warning.efficiency.handlingState === 'AVAILABLE', '处置率连 `state` 一起带出("分母为 0"与"真的是 0%"必须分得开)', ) // 工单状态分布:0–7 全铺,缺的态补 0(0 是真实计数,不是"没有数据")。 check( 'B3c-work-order-states-cover-all-eight-codes', warnings.warning.workOrderStates.length === 8 && warnings.warning.workOrderStates.map((row) => row.code).join(',') === '0,1,2,3,4,5,6,7' && warnings.warning.workOrderStates.find((row) => row.code === '5').count === 2 && warnings.warning.workOrderStates.find((row) => row.code === '1').count === 0, `0–7 全铺(8 个码):${warnings.warning.workOrderStates.map((row) => `${row.code}:${row.count}`).join(' ')}`, ) check( 'B3d-range-type-is-echoed', warnings.rangeType === 'LAST_7_DAYS', `窗口回显 ${warnings.rangeType}(验收第 1 条的「近 7 日」靠它声明)`, ) const regions = toOverviewRegions({ totals: { facilityCount: 5, lengthKm: '12.4', scale: 'medium' }, regionDistribution: [{ code: '城区A', count: 3 }, { code: '未分区', count: 2 }], }) check( 'B4-regions-come-from-the-sibling-endpoint', regions.regions.length === 2 && regions.regions[0].label === '城区A' && regions.lengthKm === 12.4, `区域分布借票 17 的端点:${regions.regions.map((row) => `${row.label}${row.count}`).join(' ')}`, ) } // ================================================================ C 视图层 { const data = { infrastructure: { facilityCount: 5, smartDeviceCount: 9, industries: [{ code: 'GAS', key: 'gas', label: '燃气', facilityCount: 2, smartDeviceCount: 4 }], excluded: {} }, regions: { regions: [{ code: '城区A', label: '城区A', count: 3 }], lengthKm: 12.4, scale: 'medium' }, monitoring: { deviceCount: 9, monitoringPointCount: 7, monitoringFacilityCount: 3, infrastructureFacilityCount: 5, onlineRate: 0.75, onlineCount: 6, onlineDenominator: 8, onlineState: 'CURRENT', deviceStatus: { online: 6, offline: 1, stale: 1, unknownOrMissing: 1, denominator: 8 }, formalIndustries: ['GAS'], metricFreshness: { status: 'CURRENT', updatedAt: 'T' }, typeDistribution: [{ industryLabel: '燃气', typeName: '燃气管线', deviceCount: 4 }], }, warningSettings: { rangeType: 'LAST_7_DAYS', warning: { family: 'WARNING', total: 8, handled: 5, pending: 2, cancelledCount: 1, unlinkedEventCount: 1, efficiency: { handlingRate: 0.625, handlingState: 'AVAILABLE', totalEvents: 8 }, workOrderStates: WORK_ORDER_STATES.map((s) => ({ code: String(s.code), label: s.name, count: 0 })), trend: [] }, alarm: { family: 'ALARM', total: 9, handled: 6, pending: 2, cancelledCount: 1, unlinkedEventCount: 0, efficiency: { handlingRate: 0.667, handlingState: 'AVAILABLE', totalEvents: 9 }, workOrderStates: WORK_ORDER_STATES.map((s) => ({ code: String(s.code), label: s.name, count: 0 })), trend: [] }, }, } const situation = situationSummaryPanel(data, ready(data)) const kpiKeys = situation.blocks[0].items.map((item) => item.key) check( 'C1-situation-kpis-cover-the-acceptance-list', ['facilityCount', 'smartDeviceCount', 'deviceCount', 'monitoringPointCount', 'onlineRate', 'warningTotal', 'alarmTotal', 'handlingRate'].every((key) => kpiKeys.includes(key)), `跨行业 KPI:${kpiKeys.join(' / ')}`, ) check( 'C1b-situation-has-both-trend-series', situation.blocks.filter((block) => block.kind === 'trend').length === 2 && situation.rangeNote.includes('近 7 日'), `预警与报警两条趋势序列(${situation.blocks.filter((b) => b.kind === 'trend').map((b) => b.label).join(' / ')})`, ) check( 'C1c-situation-handling-rate-is-a-link', situation.blocks[0].items.find((item) => item.key === 'handlingRate').link === 'EFFICIENCY' && situation.blocks[0].items.find((item) => item.key === 'warningTotal').link === 'WARNING', '当前预警/报警/处置率三个读数带联动出口(验收第 5 条)', ) // **分母为 0 不是 0%**:后端回 value="0.0000" + state=EMPTY 时界面必须显示占位符。 const zeroRate = situationSummaryPanel( { ...data, monitoring: { ...data.monitoring, onlineRate: 0, onlineDenominator: 0, onlineState: 'EMPTY' } }, ready(data), ) check( 'C1d-zero-denominator-is-not-zero-percent', zeroRate.blocks[0].items.find((item) => item.key === 'onlineRate').value === DETAIL_PLACEHOLDER && zeroRate.blocks[0].items.find((item) => item.key === 'onlineRate').hint.includes('不适用'), `分母为 0 → ${zeroRate.blocks[0].items.find((item) => item.key === 'onlineRate').value}(不是 0.0%)`, ) const infra = infrastructureSummaryPanel(data, ready(data)) check( 'C2-infrastructure-has-industry-and-region', infra.blocks.some((block) => block.label === '行业构成 · 设施') && infra.blocks.some((block) => block.label === '区域分布') && infra.blocks.find((block) => block.label === '区域分布').rows.length === 1, `设施底数 + 行业构成 + 区域分布(验收第 2 条三项齐)`, ) check( 'C2b-industry-rows-are-actionable', infra.blocks.find((block) => block.label === '行业构成 · 设施').action === 'industry' && infra.blocks.find((block) => block.label === '行业构成 · 设施').rows[0].actionKey === 'gas', '行业行带联动动作与前端筛选键(点它写全局行业筛选)', ) check( 'C2c-region-gap-is-honest-when-missing', infrastructureSummaryPanel({ infrastructure: data.infrastructure, regions: null }, ready(data)).gap.includes('base-archives'), '区域取不到时如实说明它来自哪个端点,不假装有', ) check( 'C2d-length-and-scale-come-from-the-sibling-endpoint', infra.blocks[0].items.find((item) => item.key === 'lengthKm').value === '12.4' && infra.blocks[0].items.find((item) => item.key === 'scale').value === '中型', '里程与规模取自票 17 的档案端点(本概览端点不给),英文规模枚举被翻成中文', ) const monitoring = monitoringSummaryPanel(data.monitoring, ready(data.monitoring)) const monitoringKpis = monitoring.blocks[0].items.map((item) => item.key) check( 'C3-monitoring-shows-online-rate-and-four-buckets', ['onlineRate', 'online', 'offline', 'stale', 'unknownOrMissing'].every((key) => monitoringKpis.includes(key)), `在线率 + 四态桶:${monitoringKpis.join(' / ')}(验收第 3 条)`, ) // ⚠️ 真机走查逮到的真缺陷,这条守卫就是它的护栏: // `monitoringSummaryPanel` 原先读 `data.monitoring`(以为拿到包装),而外壳传的是 // **裸领域对象** `section.data` —— 于是整块读数全是 `—`,而当时逻辑闸门全绿, // 因为**测试喂的正是那个错误的包装形状**(测试与实现犯了同一个错)。 // 所以这里必须断言"喂真实形状时**真的画出数字**",而不只是"块在不在"。 check( 'C3-contract-panel-reads-its-own-section-data', monitoringKpis.includes('onlineRate') && monitoring.blocks[0].items.find((item) => item.key === 'onlineRate').value === '75.0%' && monitoring.blocks[0].items.find((item) => item.key === 'online').value === '6' && monitoring.blocks.some((block) => block.label === '监测覆盖'), `按外壳真实的调用形状(裸 section.data)喂进去,读数真的是数字而不是占位符:在线率 ${monitoring.blocks[0].items.find((i) => i.key === 'onlineRate').value} · 在线 ${monitoring.blocks[0].items.find((i) => i.key === 'online').value}`, ) check( 'C3b-monitoring-has-coverage-and-type-distribution', monitoring.blocks.some((block) => block.label === '监测覆盖') && monitoring.blocks.some((block) => block.label === '类型分布 · 智能设备'), '监测覆盖 + 类型分布', ) // 验收第 3 条点名「故障统计」,而契约没有该桶 —— 必须如实说明,不静默略过。 check( 'C3c-fault-gap-is-declared', monitoring.gap === MONITORING_FAULT_GAP && monitoring.gap.includes('没有独立的「故障」桶'), '「故障」缺口如实写在面板上(不发明一个故障数)', ) check( 'C3d-metric-freshness-row-is-present', monitoring.blocks.some((block) => block.label === '最近采集时间'), '逐设备最近采集时间有值时才画那一行', ) const alert = alertSettingsPanel(data.warningSettings, ready(data.warningSettings)) // 只有分布块有 `label`(KPI 块的 label 在 items 上),所以先滤掉没有 label 的块。 const alertLabels = alert.blocks.map((block) => block.label).filter(Boolean) check( 'C4-alert-settings-shows-both-families', alertLabels.some((label) => label.includes('预警')) && alertLabels.some((label) => label.includes('报警')) && alert.blocks.filter((block) => block.kind === 'bars').length === 2, `预警与报警两族统计同屏(验收第 4 条:保持合同名、只读展示):${alertLabels.join(' / ')}`, ) // 与 C3 同一族护栏:喂**裸领域对象**时 KPI 必须是真的数字。 check( 'C4-contract-alert-panel-reads-its-own-section-data', alert.blocks[0].items.find((item) => item.key === 'WARNING-total').value === '8' && alert.blocks[2].items.find((item) => item.key === 'ALARM-total').value === '9', `按外壳真实的调用形状喂进去,两族总数真的画出来:预警 ${alert.blocks[0].items.find((i) => i.key === 'WARNING-total').value} 条 / 报警 ${alert.blocks[2].items.find((i) => i.key === 'ALARM-total').value} 条`, ) check( 'C4b-alert-work-order-distribution-is-eight-codes', alert.blocks.filter((block) => block.kind === 'bars').every((block) => block.rows.length === 8), '工单状态分布 0–7 全铺(8 个码,不是旧的七态)', ) check( 'C4c-alert-keeps-the-contract-name', alert.name === 'alertSettings' && alert.label === '预警设置', '面板名保持合同名「预警设置」(验收第 4 条原句)', ) // 失败/空时仍返回结构完整的视图(面板标题不能跟着数据一起消失)。 const failed = section(SECTION_STATE.FAILED, { data: null }) check( 'C5-failed-section-still-returns-a-complete-view', ['situationSummary', 'infrastructureSummary', 'monitoringSummary', 'alertSettings'].every((panelId) => { const view = panelView(panelId, null, { ...failed, loaded: true }) return view && Array.isArray(view.blocks) && view.hasContent === false && typeof view.placeholder === 'string' }), '四块在失败态都返回结构完整的视图(块在、占位说明在)', ) } // ================================================================ D 分区独立(真契约 mock 走一遍) { setActivePinia(createPinia()) const { registerCockpitMocks } = await import('../../../src/modules/cockpit/mocks/contractMocks.js') const { registerMockTransport, resetMockTransportsForTest } = await import('../../../src/modules/cockpit/services/cockpitApi.js') const { builtinConfig } = await import('../../../src/modules/cockpit/services/cockpitConfig.js') const store = useOverviewStore() store.reset() store.configure({ config: builtinConfig }) resetMockTransportsForTest() registerCockpitMocks({ scenario: 'success', handlers: { registerMockTransport, resetMockTransportsForTest } }) await store.loadPanels({ industry: '' }) const sections = store.sections check( 'D1-three-panels-load-independently', sections.infrastructureSummary.state === SECTION_STATE.SUCCESS && sections.monitoringSummary.state === SECTION_STATE.SUCCESS && sections.alertSettings.state === SECTION_STATE.SUCCESS, `三块各自成功:${['infrastructureSummary', 'monitoringSummary', 'alertSettings'].map((id) => sections[id].state).join(' / ')}`, ) check( 'D2-situation-is-composed-not-fetched', store.situationSection().state === SECTION_STATE.SUCCESS && store.situationData().warningSettings !== null && store.situationData().monitoring !== null, '态势总览由另外三块合成(它不自己发请求),故与预警设置同源', ) // 同源的结构性证明:两处读的是**同一个对象引用**,不是"两次取数期望一致"。 check( 'D2b-situation-and-alert-share-the-same-payload', store.situationData().warningSettings === sections.alertSettings.data, '态势总览与预警设置读同一份预警载荷(引用相同)—— 两处数字不可能对不上', ) check( 'D3-regions-are-merged-into-infrastructure', store.infrastructureData().regions !== null && Array.isArray(store.infrastructureData().regions.regions) && store.infrastructureData().regions.regions.length > 0, `区域分布并进基础设施分区(${((store.infrastructureData().regions || {}).regions || []).length} 行)`, ) // ⚠️ **端到端**:从 store 取数 → 经 `panelView()`(外壳用的那个入口)→ 断言读数是真的数字。 // // 为什么这条必须存在(票 20 真机走查的教训):C 段那几条是**手喂 data** 的单元断言, // 而真机那次恰恰是"单元断言与实现犯了同一个错"(都以为 data 是包装对象)→ 全绿而界面全 `—`。 // 只有走**外壳真实的调用路径**才拦得住这类错。 // // 注意每块的 data 来源**不一样**,这里必须与 `CockpitShell.vue` 的 `panelViewOf()` 逐块一致: // 态势总览与基础设施面板吃的是 store 的**合成**数据,另外两块吃 `section.data`。 // (这个"逐块不同"本身就是易错点,F6c 那条守卫专门钉它。) const shellDataSource = { situationSummary: () => store.situationData(), infrastructureSummary: () => store.infrastructureData(), monitoringSummary: () => store.sections.monitoringSummary.data, alertSettings: () => store.sections.alertSettings.data, } const endToEnd = [ ['monitoringSummary', 'onlineRate', '75.0%'], ['alertSettings', 'WARNING-total', '8'], ['infrastructureSummary', 'facilityCount', '5'], ['situationSummary', 'warningTotal', '8'], ].map(([panelId, kpiKey, expected]) => { const section = panelId === 'situationSummary' ? store.situationSection() : store.sections[panelId] const view = panelView(panelId, shellDataSource[panelId](), section) const found = (view.blocks.find((block) => block.kind === 'kpi') || { items: [] }).items.find((item) => item.key === kpiKey) return { panelId, kpiKey, expected, actual: found ? found.value : '(无此 KPI)' } }) check( 'D8-end-to-end-panels-really-draw-numbers', endToEnd.every((row) => row.actual === row.expected), `经外壳真实入口逐块读数:${endToEnd.map((r) => `${r.panelId}.${r.kpiKey}=${r.actual}`).join(' · ')}`, ) // 只坏一块:另外两块照常成功(验收:任一分区失败不遮盖其它)。 resetMockTransportsForTest() registerCockpitMocks({ scenario: 'failed', onlyPath: '/overview/monitoring', handlers: { registerMockTransport, resetMockTransportsForTest } }) const partial = useOverviewStore() partial.reset() partial.configure({ config: builtinConfig }) await partial.loadPanels({ industry: '' }) check( 'D4-one-broken-panel-does-not-cover-others', partial.sections.monitoringSummary.state !== SECTION_STATE.SUCCESS && partial.sections.infrastructureSummary.state === SECTION_STATE.SUCCESS && partial.sections.alertSettings.state === SECTION_STATE.SUCCESS, `只坏监测概览:监测 ${partial.sections.monitoringSummary.state},基础设施 ${partial.sections.infrastructureSummary.state},预警设置 ${partial.sections.alertSettings.state}`, ) // 对抗式反证:全成功场景下三块都是 success,所以上面那条"一块坏"确实有信息量。 check( 'D4b-control-all-success', sections.monitoringSummary.state === SECTION_STATE.SUCCESS, '对照组:同一份代码在 success 场景下监测概览是 success(故 D4 的判红有牙)', ) // 空场景:不伪造 0,也不把"没有数据"画成"加载失败"。 resetMockTransportsForTest() registerCockpitMocks({ scenario: 'empty', handlers: { registerMockTransport, resetMockTransportsForTest } }) const empty = useOverviewStore() empty.reset() empty.configure({ config: builtinConfig }) await empty.loadPanels({ industry: '' }) check( 'D5-empty-is-not-failure', [empty.sections.infrastructureSummary.state, empty.sections.monitoringSummary.state, empty.sections.alertSettings.state].every((state) => state === SECTION_STATE.EMPTY), `空场景三块都是 ${SECTION_STATE.EMPTY}("没有数据"与"加载失败"分得开)`, ) // 行业筛选:只有 `/overview/monitoring` 收该入参,所以它必须**真的**换内容。 resetMockTransportsForTest() registerCockpitMocks({ scenario: 'success', handlers: { registerMockTransport, resetMockTransportsForTest } }) const filtered = useOverviewStore() filtered.reset() filtered.configure({ config: builtinConfig }) await filtered.loadPanels({ industry: '' }) const allDevices = filtered.sections.monitoringSummary.data.deviceCount await filtered.loadPanels({ industry: 'gas' }) const gasDevices = filtered.sections.monitoringSummary.data.deviceCount check( 'D6-industry-filter-really-changes-the-numbers', gasDevices < allDevices && filtered.sections.monitoringSummary.data.formalIndustries.join(',') === 'GAS', `行业筛选真的重算:设备 ${allDevices} → ${gasDevices},正式范围 ${filtered.sections.monitoringSummary.data.formalIndustries.join(',')}`, ) check( 'D6b-industry-filter-does-not-refetch-unfilterable-panels', filtered.sections.infrastructureSummary.data.facilityCount === 5, '另两个端点不收行业入参,故其读数不随筛选变化(不假装生效)', ) // 趋势窗口:本票固定声明 LAST_7_DAYS,后端据此过滤桶。 check( 'D7-range-is-declared-and-echoed', OVERVIEW_RANGE === 'LAST_7_DAYS' && filtered.sections.alertSettings.data.rangeType === 'LAST_7_DAYS', `窗口 ${filtered.sections.alertSettings.data.rangeType}(不声明时后端是无界 CURRENT_BASELINE,"近 7 日"就只是句空话)`, ) } // ================================================================ E 联动 { setActivePinia(createPinia()) const store = useOverviewStore() const uiStore = useUiStore() const filterStore = useFilterStore() uiStore.setMode('overview') filterStore.setIndustry('all') const switched = store.linkToMode('warningResponse') check( 'E1-metric-link-switches-mode', switched === true && uiStore.mode === 'warningResponse', `点指标 → 一级模式切到 ${uiStore.mode}(落到真实事件列表,那里的事件带设备编码可再跳点位)`, ) check( 'E1b-link-does-not-fake-a-map-locate', typeof store.locatePoint === 'undefined' && typeof store.locateCode === 'undefined', '概览 store 不提供任何地图定位入口(本模式端点不含点位编码,定位只能伪造)', ) uiStore.setMode('overview') const linked = store.linkToIndustry('gas') check( 'E2-industry-link-writes-the-global-filter', linked === true && filterStore.industry === 'gas', `点行业行 → 全局行业筛选变成 ${filterStore.industry}(真的驱动一张图与各面板)`, ) check( 'E2b-empty-action-is-refused', store.linkToIndustry('') === false && store.linkToMode('') === false, '空动作被拒(不静默写一个空筛选或切到未定义模式)', ) filterStore.setIndustry('all') uiStore.setMode('overview') } // ================================================================ F 静态守卫 { const domain = read('domain/overviewPanels.js') const service = read('services/overviewApi.js') const storeCode = read('stores/overviewStore.js') const renderer = read('components/PanelBlocks.vue') const shell = read('CockpitShell.vue') // 注释会解释被禁的东西,直接对全文正则会拿假阳性换通过率(t18 的教训)。 const stripComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1').replace(//g, '') const domainCode = stripComments(domain) const storeClean = stripComments(storeCode) check( 'F1-domain-is-pure', !/\bfetch\s*\(|XMLHttpRequest/.test(domainCode) && !/from '\.\.\/services\//.test(domainCode), 'domain 不自己发请求、不反向依赖 service(层次不倒挂)', ) check( 'F2-service-does-not-build-absolute-urls', !/https?:\/\//.test(stripComments(service)) && !/Authorization|Bearer/.test(stripComments(service)), 'service 只给根相对路径,地址与凭据归部署层(票 04 的唯一出口)', ) // 守卫面与 t05 的 D1 **同宽**(票 20 的教训:我第一版只列了自己用到的几个词, // 于是 `rawDeviceStatistics` 这种"加了前缀的裸字段"漏过去了 —— // 变异 M12 实测全绿才发现。守卫比它要守的东西窄,就等于没守)。 // 注意用 `\w*` 前后缀:裸字段名带前缀/后缀同样是泄漏(`rawDeviceStatistics`)。 const RAW_FIELD = /\w*(monitoringPoints|unassociatedEquipment|coordinateValidation|pointStatusCode|deviceStatistics|aggregateState|timelineEntries|alarmWarningRelations|warningNo|alarmId|stateDictionary|stateReason|missingCycles|dataAgeSeconds|calibrationPeriodSeconds|staleReason|workOrders|deviceStatus|metricFreshness|workOrderStates|formalScope|typeDistribution)\w*/ check( 'F3-store-has-no-raw-backend-fields', !RAW_FIELD.test(storeClean), 'store 里零后端裸字段名(字段改名归 domain)—— 与 t05 的 D1 守卫面同宽', ) check( 'F4-store-has-no-write-operations', !/\b(POST|PUT|PATCH|DELETE)\b/.test(storeClean), '驾驶舱只读:本票不引入任何写操作', ) // 渲染层**不认识任何 panelId**(票 18 解除的耦合,本票不得长回来)。 check( 'F5-renderer-does-not-know-panel-ids', !/panelId/.test(stripComments(renderer)), '共享渲染层不认识 panelId(它只按 block.kind 与 block.action 分派)', ) check( 'F5b-renderer-has-no-api-paths-or-fetch', !/\/overview\//.test(stripComments(renderer)) && !/fetch\(/.test(stripComments(renderer)), '渲染层不拼接口路径、不发请求', ) // 外壳走同一条接缝:overview 四块必须走 PanelBlocks,而不是再长一套分支。 check( 'F6-shell-routes-overview-through-the-shared-seam', (shell.match(/v-if="hasPanelView\(panelId/g) || []).length === 2 && /!hasPanelView\(panelId\)\) return '面板占位'/.test(shell) && /return '正在取数'/.test(shell), '外壳保持两处 hasPanelView 分支与既有的三态措辞(t18 的 H9 断言面)', ) // 这一条守的是**接线本身**,不是"某个词出现过"。 // // 票 20 的教训(变异 M13 实测全绿才发现):第一版只查 `linkToMode` / `linkToIndustry` // 在文件里**出现过** —— 而它们在 `handlePanelAction` 的定义里当然出现, // 于是"把 `@panel-action` 绑定删掉"(联动出口实际断掉)根本测不出来。 // 现在改成断言**两处模板绑定**都在(左右栏各一处),以及取数入口在挂载里被调用。 check( 'F6b-shell-wires-overview-store-and-links', (shell.match(/@panel-action="handlePanelAction"/g) || []).length === 2 && /overviewStore\.loadPanels\(/.test(shell) && /useOverviewStore/.test(shell) && /overviewStore\.linkToMode\(/.test(shell) && /overviewStore\.linkToIndustry\(/.test(shell), '外壳把概览接进共享接缝:左右栏各一处 @panel-action 绑定 + 取数入口 + 两条联动出口都真的接上', ) // 这一条钉住"哪块面板吃哪份数据"—— 真机走查逮到的那类错正是**这一层错位**: // 视图函数以为 data 是包装对象、而外壳传的是裸 `section.data`,于是界面全是 `—` 而闸门全绿。 // 所以把外壳里的分派逐块钉住:合成数据的两块必须显式传合成结果,另两块走 `section.data`。 check( 'F6c-shell-per-panel-data-sources-are-pinned', /panelView\(panelId, overviewStore\.situationData\(\), section, extra\)/.test(shell) && /panelView\(panelId, overviewStore\.infrastructureData\(\), section, extra\)/.test(shell) && /return panelView\(panelId, section\.data, section, extra\)/.test(shell), '外壳逐块分派数据源:态势总览→situationData()、基础设施→infrastructureData()、其余→section.data(与各视图函数的期望一一对应)', ) check( 'F7-domain-declares-its-own-paths-nowhere', !/\/overview\//.test(domainCode), 'domain 里不出现接口路径(路径归 services)', ) check( 'F8-paths-live-in-one-place', OVERVIEW_PATHS.infrastructure === '/overview/infrastructure' && OVERVIEW_PATHS.monitoring === '/overview/monitoring' && OVERVIEW_PATHS.warningSettings === '/overview/warning-settings' && OVERVIEW_PATHS.regions === '/infrastructure/base-archives', `四个端点路径只有一处声明:${Object.values(OVERVIEW_PATHS).join(' / ')}`, ) } const passed = checks.filter(Boolean).length console.log(`\nT20 overview-panels: ${passed}/${checks.length}`) if (passed !== checks.length) process.exitCode = 1