/** * 票 24 链路回归:mock → 契约 → `pipeLayerService` → `pipeNetworkStore` → 图层计划形状。 * * 与 `t24-pipe-layer.mjs` 的分工:那一份锁**纯判定**(分类/校核/筛选), * 这一份锁**装配**——真实浏览器里管线图层能画出来,靠的就是这条链路每一环都接对了: * mock 样例 → 领域转换 → 分区状态 → store 派生视图 → `modeLayerPlan` 认得的形状。 * * 全程不联网、不起浏览器(AGENTS.md:不得自动跑 build/debug)。 */ import assert from 'node:assert/strict' import { createPinia, setActivePinia } from 'pinia' import { registerCockpitMocks } from '../../../src/modules/cockpit/mocks/contractMocks.js' import { registerMockTransport, resetMockTransportsForTest } from '../../../src/modules/cockpit/services/cockpitApi.js' import { fetchPipeNetwork, PIPE_LAYER_PATH } from '../../../src/modules/cockpit/services/pipeLayerService.js' import { usePipeNetworkStore } from '../../../src/modules/cockpit/stores/pipeNetworkStore.js' import { planModeLayers } from '../../../src/modules/cockpit/domain/modeLayerPlan.js' import { SECTION_STATE } from '../../../src/modules/cockpit/domain/sectionState.js' import { coordinateOverview, facilityCoordinateExtra } from '../../../src/modules/cockpit/domain/coordinate.js' const mockConfig = { dataSource: 'mock' } const register = (scenario) => { resetMockTransportsForTest() registerCockpitMocks({ scenario, handlers: { registerMockTransport, resetMockTransportsForTest } }) } let failed = 0 async function check(name, action) { try { await action() console.log(`PASS ${name}`) } catch (error) { failed += 1 console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`) } } setActivePinia(createPinia()) await check('F1 取数路径指向票 30 的真实端点', () => { register('success') assert.equal(PIPE_LAYER_PATH, '/infrastructure/geometry') }) await check('F2 取数成功:管线/构件/被拦下三类各自成数', async () => { register('success') const section = await fetchPipeNetwork({ config: mockConfig }) assert.equal(section.state, SECTION_STATE.SUCCESS) assert.equal(section.data.pipes.length, 6, '可上图管段:燃气 3 + 供水 2 + 排水 1') assert.equal(section.data.components.length, 5, '可上图构件:燃气 4 + 窨井 1') assert.equal(section.data.blocked.length, 3, '不得上图:未校验管段 + 零坐标管段 + 区外构件') }) await check('F3 被拦下的对象都带中文原因(不是被悄悄丢掉)', async () => { register('success') const section = await fetchPipeNetwork({ config: mockConfig }) const reasons = section.data.blocked.map((row) => row.reasonText) assert.deepEqual(reasons.sort(), ['坐标未经校验', '零坐标(经纬度均为 0)', '不在主场景区范围内'].sort()) assert.ok(section.data.blocked.every((row) => row.code), '每个被拦下的对象都要有身份') }) await check('F4 行业覆盖按三态行转述:窨井有点位但无管线几何', async () => { register('success') const section = await fetchPipeNetwork({ config: mockConfig }) assert.equal(section.data.coverage.GAS.geometryAvailable, true) assert.equal(section.data.coverage.GAS.pipeCount, 3) assert.equal(section.data.coverage.GAS.componentCount, 4) assert.equal(section.data.coverage.MANHOLE.geometryAvailable, true, '窨井有点位几何') assert.equal(section.data.coverage.MANHOLE.pipeCount, 0, '但**没有管线**几何——两件事必须分得开') assert.equal(section.data.coverage.MANHOLE.componentCount, 1) assert.ok(section.data.coverage.MANHOLE.gaps.some((gap) => gap.reason === 'NO_PIPELINE_SOURCE'), '管线缺口原因要留着') assert.ok(section.data.coverage.WATER_SUPPLY.gaps.some((gap) => gap.reason === 'NO_COMPONENT_SOURCE'), '供水无构件源要如实记') }) await check('F5 空场景收成 EMPTY,而不是「成功但没有东西」', async () => { register('empty') const section = await fetchPipeNetwork({ config: mockConfig }) assert.equal(section.state, SECTION_STATE.EMPTY) }) await check('F6 store 派生视图可直接喂给 modeLayerPlan', async () => { register('success') const store = usePipeNetworkStore() await store.load({ config: mockConfig }) assert.ok(Array.isArray(store.pipelines), 'pipelines 必须是数组(计划要 .slice())') assert.ok(Array.isArray(store.facilities), 'facilities 必须是数组') assert.equal(store.pipelines.length, 6) assert.equal(store.facilities.length, 5) assert.equal(store.blocked.length, 3) }) await check('F7 切行业只裁剪、不重取(图层即时切换)', async () => { register('success') const store = usePipeNetworkStore() await store.load({ config: mockConfig }) store.setIndustry('gas') assert.ok(store.pipelines.every((row) => row.industry === 'GAS')) assert.ok(store.facilities.every((row) => row.industry === 'GAS')) store.setIndustry('all') assert.equal(store.pipelines.length, 6, '切回全域恢复全部') }) await check('F8 接进模式计划后:管线图层 ready,不再是 degraded', async () => { register('success') const store = usePipeNetworkStore() await store.load({ config: mockConfig }) store.setIndustry('all') const plan = planModeLayers({ mode: 'infrastructure', entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines, }) assert.equal(plan.layerStates.pipelines, 'ready') assert.equal(plan.layerStates.facilities, 'ready') assert.ok(!plan.degradedNotes.some((note) => note.key === 'pipelines-unready'), '不应再有「管线数据未就绪」') }) await check('F9 取数失败时如实降级,绝不伪造空图层', async () => { const store = usePipeNetworkStore() store.configure({ config: { dataSource: 'real', api: { baseUrl: '' } } }) await store.load() assert.equal(store.state, SECTION_STATE.UNAVAILABLE) assert.equal(store.pipelines, null, '失败必须交 null:交 [] 会被计划当成"已就绪但没有管线",那是谎报') const plan = planModeLayers({ mode: 'infrastructure', entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines }) assert.equal(plan.layerStates.pipelines, 'degraded', '取不到就是 degraded,不是 ready') assert.ok(plan.degradedNotes.some((note) => note.key === 'pipelines-unready'), '必须如实说「管线数据未就绪」') }) await check('G1 设施与管线并入「无地图位置」清单,带中文原因', async () => { register('success') const store = usePipeNetworkStore() await store.load({ config: mockConfig }) const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered)) const facilityEntries = overview.blockedEntries.filter((row) => row.key.startsWith('facility|')) assert.equal(facilityEntries.length, 3, '未校验管段 + 零坐标管段 + 缺经度构件都必须在清单里') assert.ok(facilityEntries.every((row) => row.reasonText), '每条都要有中文原因') assert.ok(facilityEntries.every((row) => row.code), '每条都要有身份') }) await check('G2 徽标计数与清单条数自洽(不会出现徽标 3、清单 5)', async () => { register('success') const store = usePipeNetworkStore() await store.load({ config: mockConfig }) const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered)) assert.equal(overview.loaded.blocked, 3) assert.equal(overview.blockedEntries.length, overview.loaded.blocked, '徽标数必须等于清单条数') assert.equal(overview.loaded.renderable, 11, '可上图 = 管段 6 + 构件 5') assert.equal(overview.loaded.total, 14) }) await check('G3 几何取数失败时不得变成「零个没上图的」', async () => { const store = usePipeNetworkStore() store.configure({ config: { dataSource: 'real', api: { baseUrl: '' } } }) await store.load() const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered)) assert.equal(overview.loaded.blocked, 0, '失败既不贡献可上图也不贡献不上图——不谎报') assert.equal(overview.loaded.total, 0) }) await check('G4 不传 extra 时行为与票 07 原口径完全一致', () => { const objects = [ { kind: 'monitoring-point', code: 'P-1', coordinate: { validated: true, renderable: true } }, { kind: 'monitoring-point', code: 'P-2', coordinate: { validated: false, renderable: false } }, ] const overview = coordinateOverview(objects, null) assert.equal(overview.loaded.total, 2) assert.equal(overview.loaded.renderable, 1) assert.equal(overview.loaded.blocked, 1) assert.equal(overview.blockedEntries.length, 1) }) await check('H1 四个一级模式下管线图层显隐正确(管线随模式常驻)', async () => { register('success') const store = usePipeNetworkStore() await store.load({ config: mockConfig }) for (const mode of ['overview', 'infrastructure', 'monitoring', 'warningResponse']) { const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines }) assert.equal(plan.layerStates.pipelines, 'ready', `${mode}:管线应可见`) assert.equal(plan.pipelines.length, 6, `${mode}:管线数量应一致`) } }) await check('H2 设施图层只在总体概览/基础设施可见,切到别的模式即隐', async () => { register('success') const store = usePipeNetworkStore() await store.load({ config: mockConfig }) for (const mode of ['overview', 'infrastructure']) { const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines }) assert.equal(plan.layerStates.facilities, 'ready', `${mode}:设施应可见`) assert.ok(plan.facilities.length > 0, `${mode}:设施应有内容`) } for (const mode of ['monitoring', 'warningResponse']) { const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines }) assert.equal(plan.layerStates.facilities, 'hidden', `${mode}:设施应隐藏`) assert.equal(plan.facilities.length, 0, `${mode}:隐藏时不得留下残留对象`) } }) await check('H3 引导态(未进入系统)图层全部隐起', async () => { register('success') const store = usePipeNetworkStore() await store.load({ config: mockConfig }) const plan = planModeLayers({ mode: 'overview', entered: false, objects: [], facilities: store.facilities, pipelines: store.pipelines }) assert.equal(plan.layerStates.facilities, 'hidden') assert.equal(plan.layerStates.pipelines, 'hidden') assert.equal(plan.pipelines.length, 0) }) await check('H4 认不出的模式回落到总体概览,不产生未知图层态', () => { const plan = planModeLayers({ mode: '不存在的模式', entered: true, objects: [], facilities: [{ code: 'C1' }], pipelines: [{ code: 'P1' }] }) assert.equal(plan.mode, 'overview') assert.equal(plan.layerStates.facilities, 'ready') assert.equal(plan.layerStates.pipelines, 'ready') }) console.log(`T24 链路: ${failed ? 'FAILED' : 'passed'}`) process.exitCode = failed ? 1 : 0