/** * 票 24 离线回归:管线/设施几何图层(不联网)。 * * 覆盖三件事: * 1. **契约**:后端 GeoJSON 形状 → 领域图层的转换([票 30](../issues/30-30.md) 交付物 1); * 2. **程序化构件分类**:样条管道(多段路径)/ 角管道(弯头)/ 阀门 / 支管管道(三通), * 以及未建模类型(调压等)**如实保留原文**而不是猜成已知类别; * 3. **坐标纪律**:未校验 / 零坐标 / 缺经度 一律**不得上图**,且必须带中文原因进「无地图位置」, * 既不被丢掉,也不被"挪一下"救回来。 * * mock 样例在本脚本里现搭,不依赖 `mocks/contractMocks.js` 的注册时机, * 但形状必须与它一致(那才是浏览器里真正会吃到的载荷)。 */ import assert from 'node:assert/strict' import { toPipeNetwork, filterPipeNetwork, pipeWidth, PIPE_COMPONENT } from '../../../src/modules/cockpit/domain/pipeNetwork.js' const valid = { state: 'VALID', reason: null, renderable: true, crs: 'WGS84' } /** 真实契约的 Feature 夹具:标准 GeoJSON,**没有 `kind` 字段**(按 geometry.type 判别)。 */ const feature = (overrides) => ({ type: 'Feature', id: 'pipe_network_base:P-1', geometry: { type: 'LineString', coordinates: [[110.39, 28.44], [110.40, 28.45]] }, properties: { source: 'pipe_network_base', sourceId: 'P-1', code: 'P-1', industry: 'GAS', typeCode: 'PIPE', coordinateValidation: valid }, ...overrides, }) const component = (overrides) => ({ type: 'Feature', id: 'gas_pipe_point:C-1', geometry: { type: 'Point', coordinates: [110.395, 28.445] }, properties: { source: 'gas_pipe_point', sourceId: 'C-1', code: 'C-1', pointType: 'valve', networkId: 'P-1', industry: 'GAS', typeCode: 'GAS_PIPE_POINT', coordinateValidation: valid }, ...overrides, }) let failed = 0 function check(name, action) { try { action() console.log(`PASS ${name}`) } catch (error) { failed += 1 console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`) } } const payload = { type: 'FeatureCollection', // coverage 是三态行:同一行业可有管线/构件两行 coverage: [ { industry: 'GAS', source: 'pipe_network_base', geometryType: 'LineString', state: 'AVAILABLE', reason: 'TWO_ENDPOINTS_ONLY', recordCount: 2, geometryCount: 2, renderableCount: 2 }, { industry: 'WATER_SUPPLY', source: 'pipe_network_base', geometryType: 'LineString', state: 'AVAILABLE', reason: 'TWO_ENDPOINTS_ONLY', recordCount: 1, geometryCount: 1, renderableCount: 1 }, { industry: 'GAS', source: 'gas_pipe_point', geometryType: 'Point', state: 'AVAILABLE', reason: null, recordCount: 4, geometryCount: 4, renderableCount: 4 }, { industry: 'MANHOLE', source: null, geometryType: 'LineString', state: 'UNAVAILABLE', reason: 'NO_PIPELINE_SOURCE', recordCount: 0, geometryCount: 0, renderableCount: 0 }, ], features: [ feature({}), // 多段路径 = 样条管道的输入(真实库多为两点,但契约必须支持多段) feature({ geometry: { type: 'LineString', coordinates: [[110.39, 28.44], [110.395, 28.445], [110.40, 28.44]] }, properties: { source: 'pipe_network_base', sourceId: 'P-2', code: 'P-2', industry: 'GAS', typeCode: 'PIPE', diameter: 400, coordinateValidation: valid } }), feature({ properties: { source: 'pipe_network_base', sourceId: 'P-3', code: 'P-3', industry: 'WATER_SUPPLY', typeCode: 'PIPE', coordinateValidation: valid } }), component({}), component({ geometry: { type: 'Point', coordinates: [110.40, 28.44] }, properties: { source: 'gas_pipe_point', sourceId: 'C-2', code: 'C-2', pointType: 'elbow', networkId: 'P-1', industry: 'GAS', typeCode: 'GAS_PIPE_POINT', coordinateValidation: valid } }), component({ geometry: { type: 'Point', coordinates: [110.39, 28.44] }, properties: { source: 'gas_pipe_point', sourceId: 'C-3', code: 'C-3', pointType: 'tee', networkId: 'P-2', industry: 'GAS', typeCode: 'GAS_PIPE_POINT', coordinateValidation: valid } }), component({ geometry: { type: 'Point', coordinates: [110.41, 28.46] }, properties: { source: 'gas_pipe_point', sourceId: 'C-4', code: 'C-4', pointType: 'regulator', networkId: 'P-1', industry: 'GAS', typeCode: 'GAS_PIPE_POINT', coordinateValidation: valid } }), // 中文词表也要认(数据底座注释里是中文,真实端点实测是英文,两侧都不能漏) component({ geometry: { type: 'Point', coordinates: [110.42, 28.47] }, properties: { source: 'gas_pipe_point', sourceId: 'C-5', code: 'C-5', pointType: '阀门', networkId: 'P-1', industry: 'GAS', typeCode: 'GAS_PIPE_POINT', coordinateValidation: valid } }), ], } const network = toPipeNetwork(payload) check('B1 管段与构件分开归类,不混为一类对象', () => { assert.equal(network.pipes.length, 3) assert.equal(network.components.length, 5) assert.ok(network.pipes.every((row) => row.kind === undefined), '管段不带构件类别') }) check('B2 多段路径原样保留(样条管道的输入不被压成两点)', () => { const spline = network.pipes.find((row) => row.code === 'P-2') assert.equal(spline.positions.length, 3) }) check('B3 四类构件按后端真实属性分类(英文 pointType,实测形状)', () => { const byId = Object.fromEntries(network.components.map((row) => [row.code, row.kind])) assert.equal(byId['C-1'], PIPE_COMPONENT.VALVE, 'valve → 阀门') assert.equal(byId['C-2'], PIPE_COMPONENT.ELBOW, 'elbow → 角管道') assert.equal(byId['C-3'], PIPE_COMPONENT.BRANCH, 'tee → 支管管道') assert.equal(byId['C-4'], PIPE_COMPONENT.OTHER, 'regulator 未建模 → other') assert.equal(byId['C-5'], PIPE_COMPONENT.VALVE, '中文「阀门」也要认') }) check('B4 未建模的构件类型保留后端原文,不被猜成已知类别', () => { const regulator = network.components.find((row) => row.code === 'C-4') assert.equal(regulator.label, 'regulator') }) check('B5 构件记录它属于哪条管线(不做拓扑推断)', () => { const valve = network.components.find((row) => row.code === 'C-1') assert.equal(valve.networkId, 'P-1') }) check('B6 行业覆盖按后端三态行转述,缺口如实记录', () => { assert.equal(network.coverage.GAS.geometryAvailable, true) assert.equal(network.coverage.GAS.pipeCount, 2, '管线段取 LineString 行') assert.equal(network.coverage.GAS.componentCount, 4, '构件段取 Point 行') assert.equal(network.coverage.MANHOLE.geometryAvailable, false, '窨井只有 UNAVAILABLE 行 → 如实说无') assert.ok(network.coverage.MANHOLE.gaps.some((gap) => gap.reason === 'NO_PIPELINE_SOURCE'), '缺口原因要留着,界面才说得清为什么') assert.equal(network.coverage.DRAINAGE.geometryAvailable, false, '后端没点名排水 → 不假装有') assert.equal(network.coverage.DRAINAGE.pipeCount, 0) }) check('C1 未校验坐标不得上图,且带中文原因', () => { const blocked = toPipeNetwork({ features: [feature({ properties: { sourceId: 'X-1', code: 'X-1', industry: 'GAS', coordinateValidation: { state: 'UNVERIFIED', reason: 'UNVERIFIED_COORDINATE', renderable: false } } })] }) assert.equal(blocked.pipes.length, 0) assert.equal(blocked.blocked.length, 1) assert.equal(blocked.blocked[0].reasonText, '坐标未经校验') }) check('C2 零坐标不得上图', () => { const blocked = toPipeNetwork({ features: [feature({ properties: { sourceId: 'X-2', code: 'X-2', industry: 'GAS', coordinateValidation: { state: 'INVALID', reason: 'ZERO_COORDINATE', renderable: false } } })] }) assert.equal(blocked.pipes.length, 0) assert.equal(blocked.blocked[0].reasonText, '零坐标(经纬度均为 0)') }) check('C3 认不出的原因回「无地图位置」,不把原因码甩给界面', () => { const blocked = toPipeNetwork({ features: [feature({ properties: { sourceId: 'X-3', code: 'X-3', industry: 'GAS', coordinateValidation: { state: 'INVALID', reason: 'SOMETHING_NEW', renderable: false } } })] }) assert.equal(blocked.blocked[0].reasonText, '无地图位置') }) check('C4 点数不足的管段不得渲染(不补点、不截断成一点)', () => { const blocked = toPipeNetwork({ features: [feature({ geometry: { type: 'LineString', coordinates: [[110.39, 28.44]] } })] }) assert.equal(blocked.pipes.length, 0) assert.equal(blocked.blocked.length, 1) }) check('C5 缺少校验结论一律拦下("没说不"不等于"可以画")', () => { const blocked = toPipeNetwork({ features: [feature({ properties: { sourceId: 'X-4', code: 'X-4', industry: 'GAS' } })] }) assert.equal(blocked.pipes.length, 0) assert.equal(blocked.blocked[0].reasonText, '坐标未校验') }) check('C6 真实契约的非法坐标是 geometry:null,必须按类型正确归类且拦下', () => { // 后端对非法坐标回 geometry:null(原始坐标在 sourceCoordinates)——不能因此被当成管段或漏掉 const componentNull = toPipeNetwork({ features: [component({ geometry: null, properties: { source: 'gas_pipe_point', sourceId: 'N-1', code: 'N-1', pointType: 'valve', industry: 'GAS', typeCode: 'GAS_PIPE_POINT', coordinateValidation: { state: 'INVALID', reason: 'OUTSIDE_PRIMARY_AREA', renderable: false } } })] }) assert.equal(componentNull.components.length, 0) assert.equal(componentNull.blocked.length, 1) assert.equal(componentNull.blocked[0].kind, 'component', 'geometry:null 时按 typeCode 判为构件') assert.equal(componentNull.blocked[0].reasonText, '不在主场景区范围内') const pipeNull = toPipeNetwork({ features: [feature({ geometry: null, properties: { source: 'pipe_network_base', sourceId: 'N-2', code: 'N-2', industry: 'DRAINAGE', typeCode: 'PIPE', coordinateValidation: { state: 'INVALID', reason: 'OUTSIDE_PRIMARY_AREA', renderable: false } } })] }) assert.equal(pipeNull.pipes.length, 0) assert.equal(pipeNull.blocked[0].kind, 'pipe', 'geometry:null 时按 typeCode 判为管段') }) check('C7 新增的主场景区外原因有专属中文文案(后端 8013a207 引入)', () => { // 该 reason 已在真实数据里出现。若词表没有它,界面只会说笼统的「无地图位置」, // 值班人员就看不出"这条是因为落在主场景区外"——与「坐标未校验」是两回事。 const blocked = toPipeNetwork({ features: [component({ geometry: null, properties: { source: 'gas_pipe_point', sourceId: 'N-3', code: 'N-3', pointType: 'valve', industry: 'GAS', typeCode: 'GAS_PIPE_POINT', coordinateValidation: { state: 'INVALID', reason: 'OUTSIDE_PRIMARY_AREA', renderable: false } } })] }) const text = blocked.blocked[0].reasonText assert.notEqual(text, '无地图位置', '必须有专属文案,不能退化成笼统兜底') assert.ok(text.includes('主场景区'), `文案要点明原因,实际:${text}`) }) check('D1 行业筛选只留该行业,图层过滤只此一处', () => { const gas = filterPipeNetwork(network, { industry: 'gas' }) assert.ok(gas.pipes.every((row) => row.industry === 'GAS')) assert.ok(gas.components.every((row) => row.industry === 'GAS')) }) check('D2 全域筛选不裁剪', () => { const all = filterPipeNetwork(network, { industry: 'all' }) assert.equal(all.pipes.length, network.pipes.length) }) check('D3 片区筛选如实标记为未作用到管线(管线无片区字段)', () => { assert.equal(filterPipeNetwork(network, { industry: 'all' }).districtApplied, false) }) check('E1 管径分档映射线宽,缺管径不炸', () => { assert.equal(pipeWidth(null), 2) assert.equal(pipeWidth(200), 2) assert.equal(pipeWidth(400), 3) assert.equal(pipeWidth(800), 4) assert.equal(pipeWidth(1200), 6) }) check('E2 空载荷不抛,退化为空图层', () => { const empty = toPipeNetwork(null) assert.deepEqual(empty.pipes, []) assert.deepEqual(empty.components, []) assert.equal(empty.coverage.GAS.geometryAvailable, false) }) console.log(`T24 管线图层: ${failed ? 'FAILED' : 'passed'}`) process.exitCode = failed ? 1 : 0