| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- /**
- * T42 契约守卫:预警设置面板消费**预警自身生命周期**契约(后端 `b047a60d`)。
- *
- * 守什么(票 42 验收):
- * 1. 预警族工单口径字段**原样 null**(不适用 ≠ 0 —— `numberish` 绝不造 0,
- * 视图显示占位符,领域层不得补假零条);
- * 2. 预警族四组分布(状态六桶/等级/行业/片区)直读透传,且 `sum(桶)==total` 自洽;
- * 3. 报警族保留工单口径(0–7 全铺、工单口径处置率)并**并存** `alarm_status` 分布;
- * 4. 空态:两族 EMPTY、已解除率「不适用」,分布仍全铺计数 0(不是没有数据)。
- *
- * 数值用基线卡片 §4.2 的**真实复验值**(2026-10-14 联调库:预警 31 / 报警 66)——
- * 守的是线上真实跑的那份契约,不是另编的玩具形状。mock(contractMocks)侧的同构
- * 由 t20 的 D8 端到端(同一领域函数消费 mock 载荷)覆盖。
- *
- * 用法:node .scratch/cockpit-real-data-wayfinding/verify/t42-warning-settings-contract.mjs
- */
- import assert from 'node:assert/strict'
- import { toOverviewWarningSettings, alertSettingsPanel } from '../../../src/modules/cockpit/domain/overviewPanels.js'
- import { handlingRateView } from '../../../src/modules/cockpit/domain/warningResponsePanels.js'
- // ---- 基线卡片 §4.2 的真实载荷形状(LAST_7_DAYS 实测,2026-10-14) ----------------
- const REAL_PAYLOAD = {
- requestId: 't42', apiVersion: 'v1', readOnly: true, displayName: '预警设置',
- selectedMode: 'WARNING', timezone: 'Asia/Shanghai',
- range: { type: 'LAST_7_DAYS', start: '2026-10-07T12:16:17+08:00', end: '2026-10-14T12:16:17+08:00' },
- updatedAt: '2026-10-14T11:37:12+08:00',
- freshness: { status: 'CURRENT', reason: 'READ_ONLY_AGGREGATE', updatedAt: '2026-10-14T11:37:12+08:00', serverUpdatedAt: '2026-10-14T12:16:17+08:00' },
- warning: {
- eventFamily: 'WARNING', state: 'READY', stateReason: 'WARNINGS_RESOLVED', total: 31,
- handled: null, pending: null, cancelledCount: null, unlinkedEventCount: null, invalidWorkOrderStateCount: null,
- handlingEfficiency: { numerator: 19, denominator: 31, value: '0.6129', state: 'AVAILABLE', reason: null },
- workOrderStates: null,
- statusDistribution: [
- { code: 'DRAFT', label: '草稿', count: 2 }, { code: 'PENDING', label: '待办', count: 4 },
- { code: 'RECEIVED', label: '已接收', count: 0 }, { code: 'PROCESSING', label: '处置中', count: 4 },
- { code: 'HANDLED', label: '已处置', count: 2 }, { code: 'CLOSED', label: '已解除', count: 19 },
- ],
- levelDistribution: [
- { code: '1', label: '1级', count: 13 }, { code: '2', label: '2级', count: 5 },
- { code: '3', label: '3级', count: 1 }, { code: '4', label: '4级', count: 12 },
- ],
- industryDistribution: [
- { code: 'GAS', label: '燃气', count: 0 }, { code: 'WATER_SUPPLY', label: '供水', count: 2 },
- { code: 'DRAINAGE', label: '排水', count: 24 }, { code: 'MANHOLE', label: '窨井', count: 1 },
- { code: 'UNKNOWN', label: '未知', count: 4 },
- ],
- districtDistribution: [
- { code: '0', label: '城南', count: 24 }, { code: '1', label: '城北', count: 1 },
- { code: '2', label: '太常', count: 2 }, { code: 'UNASSIGNED', label: '未分区', count: 4 },
- ],
- trend: [
- { bucket: '2026-10-10', total: 16, handled: 14, handlingEfficiency: { numerator: 14, denominator: 16, value: '0.8750', state: 'AVAILABLE', reason: null } },
- { bucket: '2026-10-09', total: 3, handled: 2, handlingEfficiency: { numerator: 2, denominator: 3, value: '0.6667', state: 'AVAILABLE', reason: null } },
- { bucket: '2026-10-14', total: 2, handled: 0, handlingEfficiency: { numerator: 0, denominator: 2, value: '0.0000', state: 'AVAILABLE', reason: null } },
- ],
- },
- alarm: {
- eventFamily: 'ALARM', state: 'READY', stateReason: 'EVENTS_RESOLVED', total: 66,
- handled: 64, pending: 0, cancelledCount: 0, unlinkedEventCount: 65, invalidWorkOrderStateCount: 0,
- handlingEfficiency: { numerator: 1, denominator: 5, value: '0.2000', state: 'AVAILABLE', reason: null },
- workOrderStates: { 0: 0, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 0, 7: 1 },
- statusDistribution: [
- { code: 'UNHANDLED', label: '未处理', count: 2 }, { code: 'HANDLED', label: '已处理', count: 64 },
- ],
- levelDistribution: [
- { code: '1', label: '1级', count: 0 }, { code: '2', label: '2级', count: 0 },
- { code: '3', label: '3级', count: 0 }, { code: '4', label: '4级', count: 66 },
- ],
- industryDistribution: [
- { code: 'GAS', label: '燃气', count: 0 }, { code: 'WATER_SUPPLY', label: '供水', count: 0 },
- { code: 'DRAINAGE', label: '排水', count: 66 }, { code: 'MANHOLE', label: '窨井', count: 0 },
- ],
- districtDistribution: [
- { code: '0', label: '城南', count: 66 }, { code: '1', label: '城北', count: 0 },
- { code: '2', label: '太常', count: 0 }, { code: 'UNASSIGNED', label: '未分区', count: 0 },
- ],
- trend: [
- { bucket: '2026-10-10', total: 4, handled: 0, handlingEfficiency: { numerator: 0, denominator: 4, value: '0.0000', state: 'AVAILABLE', reason: null } },
- { bucket: '2026-10-12', total: 62, handled: 1, handlingEfficiency: { numerator: 1, denominator: 62, value: '0.0161', state: 'AVAILABLE', reason: null } },
- ],
- },
- mapFilter: { eventFamily: 'WARNING', label: '预警' },
- }
- // 空场景:两族都空;分布仍全铺计数 0(后端契约如此),工单口径字段按族各自缺席。
- const EMPTY_PAYLOAD = {
- ...REAL_PAYLOAD,
- updatedAt: '2026-10-14T12:00:00+08:00',
- warning: {
- ...REAL_PAYLOAD.warning, state: 'EMPTY', stateReason: 'NO_EVENTS', total: 0,
- handlingEfficiency: { numerator: 0, denominator: 0, value: '0.0000', state: 'EMPTY', reason: 'NO_WARNINGS' },
- statusDistribution: REAL_PAYLOAD.warning.statusDistribution.map((row) => ({ ...row, count: 0 })),
- levelDistribution: REAL_PAYLOAD.warning.levelDistribution.map((row) => ({ ...row, count: 0 })),
- industryDistribution: REAL_PAYLOAD.warning.industryDistribution.map((row) => ({ ...row, count: 0 })),
- districtDistribution: REAL_PAYLOAD.warning.districtDistribution.map((row) => ({ ...row, count: 0 })),
- trend: [],
- },
- alarm: {
- ...REAL_PAYLOAD.alarm, state: 'EMPTY', stateReason: 'NO_EVENTS', total: 0,
- handled: 0, pending: 0, cancelledCount: 0, unlinkedEventCount: 0, invalidWorkOrderStateCount: 0,
- handlingEfficiency: { numerator: 0, denominator: 0, value: '0.0000', state: 'EMPTY', reason: 'NO_ELIGIBLE_WORK_ORDERS' },
- workOrderStates: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0 },
- statusDistribution: REAL_PAYLOAD.alarm.statusDistribution.map((row) => ({ ...row, count: 0 })),
- levelDistribution: REAL_PAYLOAD.alarm.levelDistribution.map((row) => ({ ...row, count: 0 })),
- industryDistribution: REAL_PAYLOAD.alarm.industryDistribution.map((row) => ({ ...row, count: 0 })),
- districtDistribution: REAL_PAYLOAD.alarm.districtDistribution.map((row) => ({ ...row, count: 0 })),
- trend: [],
- },
- }
- const section = { state: 'SUCCESS', loaded: true }
- const ready = (data) => ({ ...section, data })
- // ================================================================ 1 领域层
- const data = toOverviewWarningSettings(REAL_PAYLOAD)
- // 验收 1:预警族工单口径原样 null —— null 是"不适用",不是 0。
- assert.equal(data.warning.workOrderStates, null, '预警族 workOrderStates 必须原样 null(不补假零条)')
- assert.equal(data.warning.handled, null, '预警族 handled 必须原样 null')
- assert.equal(data.warning.pending, null, '预警族 pending 必须原样 null')
- assert.equal(data.warning.cancelledCount, null, '预警族 cancelledCount 必须原样 null')
- assert.equal(data.warning.unlinkedEventCount, null, '预警族 unlinkedEventCount 必须原样 null')
- assert.equal(data.warning.invalidWorkOrderStateCount, null, '预警族 invalidWorkOrderStateCount 必须原样 null')
- // 验收 2:四组分布直读透传且自洽(sum(桶)==total)。
- const sum = (rows) => rows.reduce((total, row) => total + row.count, 0)
- assert.equal(data.warning.statusDistribution.length, 6, '状态六桶全铺')
- assert.equal(sum(data.warning.statusDistribution), 31, '状态分布和=总数')
- assert.equal(sum(data.warning.levelDistribution), 31, '等级分布和=总数')
- assert.equal(sum(data.warning.industryDistribution), 31, '行业分布和=总数')
- assert.equal(sum(data.warning.districtDistribution), 31, '片区分布和=总数')
- assert.ok(data.warning.industryDistribution.some((row) => row.code === 'UNKNOWN' && row.label === '未知'), 'UNKNOWN 桶 label 原样透传')
- assert.ok(data.warning.districtDistribution.some((row) => row.code === 'UNASSIGNED' && row.label === '未分区'), 'UNASSIGNED 桶恒在')
- // 已解除率=已解除 ÷ 预警总数(19/31),语义与工单处置率分得开。
- assert.equal(data.warning.efficiency.numerator, 19)
- assert.equal(data.warning.efficiency.denominator, 31)
- assert.equal(data.warning.efficiency.handlingState, 'AVAILABLE')
- // 验收 3:报警族保留工单口径并并存自身生命周期。
- assert.ok(Array.isArray(data.alarm.workOrderStates) && data.alarm.workOrderStates.length === 8, '报警族工单 0–7 全铺')
- assert.equal(data.alarm.workOrderStates.find((row) => row.code === '5').count, 1, '报警族工单状态 5 计数来自后端')
- assert.equal(sum(data.alarm.statusDistribution), 66, '报警自身生命周期分布和=总数')
- assert.equal(data.alarm.statusDistribution.find((row) => row.code === 'HANDLED').count, 64, '报警已处理(alarm_status)')
- assert.equal(data.alarm.efficiency.numerator, 1, '报警处置率仍是工单口径(分子=状态 5 工单)')
- assert.equal(data.alarm.efficiency.denominator, 5, '报警处置率分母=状态 0–6 工单(7 张排除已取消 1 张)')
- // ================================================================ 2 视图层
- const view = alertSettingsPanel(data, ready(data))
- const kpiItems = view.blocks.flatMap((block) => block.items || [])
- const labels = view.blocks.map((block) => block.label).filter(Boolean)
- // 预警族 KPI 只剩 总数 + 已解除率(工单口径读数不再出现)。
- const warningKpiKeys = view.blocks[0].items.map((item) => item.key)
- assert.deepEqual(warningKpiKeys, ['WARNING-total', 'WARNING-rate'], '预警族 KPI=总数 + 已解除率')
- assert.equal(view.blocks[0].items.find((item) => item.key === 'WARNING-rate').label, '已解除率')
- // 空值显示占位符(不适用 ≠ 0):领域层 null 经 valueText 是 '—',
- // 这里直接断领域对象不携带 0(视图的 valueText 是既定纪律,t20 已守)。
- assert.notEqual(data.warning.handled, 0, 'null 不得被数成 0')
- // 分布块:预警四组 + 报警工单分布 + 报警四组;工单分布块只属于报警族。
- assert.ok(labels.includes('预警 · 生命周期状态'), '预警生命周期状态块')
- assert.equal(view.blocks.find((block) => block.label === '预警 · 生命周期状态').rows.length, 6, '状态六桶画满')
- assert.ok(labels.includes('预警 · 行业分布') && labels.includes('预警 · 片区分布'), '预警行业/片区分布块')
- assert.ok(labels.includes('报警 · 工单状态分布'), '报警工单分布块保留(工单口径)')
- assert.equal(view.blocks.find((block) => block.label === '报警 · 工单状态分布').rows.length, 8, '报警工单 0–7 画满')
- assert.ok(labels.includes('报警 · 生命周期状态'), '报警生命周期状态块并存')
- assert.equal(view.blocks.filter((block) => (block.label || '').startsWith('预警 · 工单')).length, 0, '预警族没有工单口径块')
- // 已解除率的值与 hint(票 20 单源判据:不适用不是 0%)。
- const rate = view.blocks[0].items.find((item) => item.key === 'WARNING-rate')
- assert.equal(rate.value, '61.3%', '已解除率 19/31 显示 61.3%')
- assert.equal(rate.hint, '已解除 19 / 预警总数 31', 'hint 写明分子分母,不冒充工单处置率')
- // ================================================================ 3 空态
- const emptyData = toOverviewWarningSettings(EMPTY_PAYLOAD)
- const emptyView = alertSettingsPanel(emptyData, ready(emptyData))
- assert.equal(emptyData.warning.state, 'EMPTY')
- assert.equal(handlingRateView(emptyData.warning.efficiency), '不适用', '空态已解除率=不适用(不是 0.0%)')
- assert.equal(emptyView.blocks[0].items.find((item) => item.key === 'WARNING-rate').value, '不适用')
- assert.equal(emptyView.blocks[0].items.find((item) => item.key === 'WARNING-rate').hint, '当前范围没有预警,已解除率不适用')
- assert.equal(emptyData.warning.workOrderStates, null, '空态下预警族工单口径仍是 null(族口径不随数据多少变)')
- assert.equal(sum(emptyData.warning.statusDistribution), 0, '空态分布全铺计数 0(真实计数,不是缺桶)')
- assert.equal(emptyData.alarm.efficiency.handlingReason, 'NO_ELIGIBLE_WORK_ORDERS', '报警族空态原因码透传')
- console.log('T42 warning-settings lifecycle contract: null-fidelity, four-distribution self-consistency, alarm work-order retention, empty-state not-applicable passed')
|