t24-pipe-layer-flow.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. /**
  2. * 票 24 链路回归:mock → 契约 → `pipeLayerService` → `pipeNetworkStore` → 图层计划形状。
  3. *
  4. * 与 `t24-pipe-layer.mjs` 的分工:那一份锁**纯判定**(分类/校核/筛选),
  5. * 这一份锁**装配**——真实浏览器里管线图层能画出来,靠的就是这条链路每一环都接对了:
  6. * mock 样例 → 领域转换 → 分区状态 → store 派生视图 → `modeLayerPlan` 认得的形状。
  7. *
  8. * 全程不联网、不起浏览器(AGENTS.md:不得自动跑 build/debug)。
  9. */
  10. import assert from 'node:assert/strict'
  11. import { createPinia, setActivePinia } from 'pinia'
  12. import { registerCockpitMocks } from '../../../src/modules/cockpit/mocks/contractMocks.js'
  13. import { registerMockTransport, resetMockTransportsForTest } from '../../../src/modules/cockpit/services/cockpitApi.js'
  14. import { fetchPipeNetwork, PIPE_LAYER_PATH } from '../../../src/modules/cockpit/services/pipeLayerService.js'
  15. import { usePipeNetworkStore } from '../../../src/modules/cockpit/stores/pipeNetworkStore.js'
  16. import { planModeLayers } from '../../../src/modules/cockpit/domain/modeLayerPlan.js'
  17. import { SECTION_STATE } from '../../../src/modules/cockpit/domain/sectionState.js'
  18. import { coordinateOverview, facilityCoordinateExtra } from '../../../src/modules/cockpit/domain/coordinate.js'
  19. const mockConfig = { dataSource: 'mock' }
  20. const register = (scenario) => {
  21. resetMockTransportsForTest()
  22. registerCockpitMocks({ scenario, handlers: { registerMockTransport, resetMockTransportsForTest } })
  23. }
  24. let failed = 0
  25. async function check(name, action) {
  26. try {
  27. await action()
  28. console.log(`PASS ${name}`)
  29. } catch (error) {
  30. failed += 1
  31. console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`)
  32. }
  33. }
  34. setActivePinia(createPinia())
  35. await check('F1 取数路径指向票 30 的真实端点', () => {
  36. register('success')
  37. assert.equal(PIPE_LAYER_PATH, '/infrastructure/geometry')
  38. })
  39. await check('F2 取数成功:管线/构件/被拦下三类各自成数', async () => {
  40. register('success')
  41. const section = await fetchPipeNetwork({ config: mockConfig })
  42. assert.equal(section.state, SECTION_STATE.SUCCESS)
  43. assert.equal(section.data.pipes.length, 6, '可上图管段:燃气 3 + 供水 2 + 排水 1')
  44. assert.equal(section.data.components.length, 5, '可上图构件:燃气 4 + 窨井 1')
  45. assert.equal(section.data.blocked.length, 3, '不得上图:未校验管段 + 零坐标管段 + 区外构件')
  46. })
  47. await check('F3 被拦下的对象都带中文原因(不是被悄悄丢掉)', async () => {
  48. register('success')
  49. const section = await fetchPipeNetwork({ config: mockConfig })
  50. const reasons = section.data.blocked.map((row) => row.reasonText)
  51. assert.deepEqual(reasons.sort(), ['坐标未经校验', '零坐标(经纬度均为 0)', '不在主场景区范围内'].sort())
  52. assert.ok(section.data.blocked.every((row) => row.code), '每个被拦下的对象都要有身份')
  53. })
  54. await check('F4 行业覆盖按三态行转述:窨井有点位但无管线几何', async () => {
  55. register('success')
  56. const section = await fetchPipeNetwork({ config: mockConfig })
  57. assert.equal(section.data.coverage.GAS.geometryAvailable, true)
  58. assert.equal(section.data.coverage.GAS.pipeCount, 3)
  59. assert.equal(section.data.coverage.GAS.componentCount, 4)
  60. assert.equal(section.data.coverage.MANHOLE.geometryAvailable, true, '窨井有点位几何')
  61. assert.equal(section.data.coverage.MANHOLE.pipeCount, 0, '但**没有管线**几何——两件事必须分得开')
  62. assert.equal(section.data.coverage.MANHOLE.componentCount, 1)
  63. assert.ok(section.data.coverage.MANHOLE.gaps.some((gap) => gap.reason === 'NO_PIPELINE_SOURCE'), '管线缺口原因要留着')
  64. assert.ok(section.data.coverage.WATER_SUPPLY.gaps.some((gap) => gap.reason === 'NO_COMPONENT_SOURCE'), '供水无构件源要如实记')
  65. })
  66. await check('F5 空场景收成 EMPTY,而不是「成功但没有东西」', async () => {
  67. register('empty')
  68. const section = await fetchPipeNetwork({ config: mockConfig })
  69. assert.equal(section.state, SECTION_STATE.EMPTY)
  70. })
  71. await check('F6 store 派生视图可直接喂给 modeLayerPlan', async () => {
  72. register('success')
  73. const store = usePipeNetworkStore()
  74. await store.load({ config: mockConfig })
  75. assert.ok(Array.isArray(store.pipelines), 'pipelines 必须是数组(计划要 .slice())')
  76. assert.ok(Array.isArray(store.facilities), 'facilities 必须是数组')
  77. assert.equal(store.pipelines.length, 6)
  78. assert.equal(store.facilities.length, 5)
  79. assert.equal(store.blocked.length, 3)
  80. })
  81. await check('F7 切行业只裁剪、不重取(图层即时切换)', async () => {
  82. register('success')
  83. const store = usePipeNetworkStore()
  84. await store.load({ config: mockConfig })
  85. store.setIndustry('gas')
  86. assert.ok(store.pipelines.every((row) => row.industry === 'GAS'))
  87. assert.ok(store.facilities.every((row) => row.industry === 'GAS'))
  88. store.setIndustry('all')
  89. assert.equal(store.pipelines.length, 6, '切回全域恢复全部')
  90. })
  91. await check('F8 接进模式计划后:管线图层 ready,不再是 degraded', async () => {
  92. register('success')
  93. const store = usePipeNetworkStore()
  94. await store.load({ config: mockConfig })
  95. store.setIndustry('all')
  96. const plan = planModeLayers({
  97. mode: 'infrastructure',
  98. entered: true,
  99. objects: [],
  100. facilities: store.facilities,
  101. pipelines: store.pipelines,
  102. })
  103. assert.equal(plan.layerStates.pipelines, 'ready')
  104. assert.equal(plan.layerStates.facilities, 'ready')
  105. assert.ok(!plan.degradedNotes.some((note) => note.key === 'pipelines-unready'), '不应再有「管线数据未就绪」')
  106. })
  107. await check('F9 取数失败时如实降级,绝不伪造空图层', async () => {
  108. const store = usePipeNetworkStore()
  109. store.configure({ config: { dataSource: 'real', api: { baseUrl: '' } } })
  110. await store.load()
  111. assert.equal(store.state, SECTION_STATE.UNAVAILABLE)
  112. assert.equal(store.pipelines, null, '失败必须交 null:交 [] 会被计划当成"已就绪但没有管线",那是谎报')
  113. const plan = planModeLayers({ mode: 'infrastructure', entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  114. assert.equal(plan.layerStates.pipelines, 'degraded', '取不到就是 degraded,不是 ready')
  115. assert.ok(plan.degradedNotes.some((note) => note.key === 'pipelines-unready'), '必须如实说「管线数据未就绪」')
  116. })
  117. await check('G1 设施与管线并入「无地图位置」清单,带中文原因', async () => {
  118. register('success')
  119. const store = usePipeNetworkStore()
  120. await store.load({ config: mockConfig })
  121. const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered))
  122. const facilityEntries = overview.blockedEntries.filter((row) => row.key.startsWith('facility|'))
  123. assert.equal(facilityEntries.length, 3, '未校验管段 + 零坐标管段 + 缺经度构件都必须在清单里')
  124. assert.ok(facilityEntries.every((row) => row.reasonText), '每条都要有中文原因')
  125. assert.ok(facilityEntries.every((row) => row.code), '每条都要有身份')
  126. })
  127. await check('G2 徽标计数与清单条数自洽(不会出现徽标 3、清单 5)', async () => {
  128. register('success')
  129. const store = usePipeNetworkStore()
  130. await store.load({ config: mockConfig })
  131. const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered))
  132. assert.equal(overview.loaded.blocked, 3)
  133. assert.equal(overview.blockedEntries.length, overview.loaded.blocked, '徽标数必须等于清单条数')
  134. assert.equal(overview.loaded.renderable, 11, '可上图 = 管段 6 + 构件 5')
  135. assert.equal(overview.loaded.total, 14)
  136. })
  137. await check('G3 几何取数失败时不得变成「零个没上图的」', async () => {
  138. const store = usePipeNetworkStore()
  139. store.configure({ config: { dataSource: 'real', api: { baseUrl: '' } } })
  140. await store.load()
  141. const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered))
  142. assert.equal(overview.loaded.blocked, 0, '失败既不贡献可上图也不贡献不上图——不谎报')
  143. assert.equal(overview.loaded.total, 0)
  144. })
  145. await check('G4 不传 extra 时行为与票 07 原口径完全一致', () => {
  146. const objects = [
  147. { kind: 'monitoring-point', code: 'P-1', coordinate: { validated: true, renderable: true } },
  148. { kind: 'monitoring-point', code: 'P-2', coordinate: { validated: false, renderable: false } },
  149. ]
  150. const overview = coordinateOverview(objects, null)
  151. assert.equal(overview.loaded.total, 2)
  152. assert.equal(overview.loaded.renderable, 1)
  153. assert.equal(overview.loaded.blocked, 1)
  154. assert.equal(overview.blockedEntries.length, 1)
  155. })
  156. await check('H1 四个一级模式下管线图层显隐正确(管线随模式常驻)', async () => {
  157. register('success')
  158. const store = usePipeNetworkStore()
  159. await store.load({ config: mockConfig })
  160. for (const mode of ['overview', 'infrastructure', 'monitoring', 'warningResponse']) {
  161. const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  162. assert.equal(plan.layerStates.pipelines, 'ready', `${mode}:管线应可见`)
  163. assert.equal(plan.pipelines.length, 6, `${mode}:管线数量应一致`)
  164. }
  165. })
  166. await check('H2 设施图层只在总体概览/基础设施可见,切到别的模式即隐', async () => {
  167. register('success')
  168. const store = usePipeNetworkStore()
  169. await store.load({ config: mockConfig })
  170. for (const mode of ['overview', 'infrastructure']) {
  171. const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  172. assert.equal(plan.layerStates.facilities, 'ready', `${mode}:设施应可见`)
  173. assert.ok(plan.facilities.length > 0, `${mode}:设施应有内容`)
  174. }
  175. for (const mode of ['monitoring', 'warningResponse']) {
  176. const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  177. assert.equal(plan.layerStates.facilities, 'hidden', `${mode}:设施应隐藏`)
  178. assert.equal(plan.facilities.length, 0, `${mode}:隐藏时不得留下残留对象`)
  179. }
  180. })
  181. await check('H3 引导态(未进入系统)图层全部隐起', async () => {
  182. register('success')
  183. const store = usePipeNetworkStore()
  184. await store.load({ config: mockConfig })
  185. const plan = planModeLayers({ mode: 'overview', entered: false, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  186. assert.equal(plan.layerStates.facilities, 'hidden')
  187. assert.equal(plan.layerStates.pipelines, 'hidden')
  188. assert.equal(plan.pipelines.length, 0)
  189. })
  190. await check('H4 认不出的模式回落到总体概览,不产生未知图层态', () => {
  191. const plan = planModeLayers({ mode: '不存在的模式', entered: true, objects: [], facilities: [{ code: 'C1' }], pipelines: [{ code: 'P1' }] })
  192. assert.equal(plan.mode, 'overview')
  193. assert.equal(plan.layerStates.facilities, 'ready')
  194. assert.equal(plan.layerStates.pipelines, 'ready')
  195. })
  196. console.log(`T24 链路: ${failed ? 'FAILED' : 'passed'}`)
  197. process.exitCode = failed ? 1 : 0