| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- /**
- * 票 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
|