t24-pipe-layer-flow.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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 mock 注册了几何端点', () => {
  36. register('success')
  37. assert.equal(PIPE_LAYER_PATH, '/map-facilities')
  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.MANHOLE.geometryAvailable, false)
  59. })
  60. await check('F5 空场景收成 EMPTY,而不是「成功但没有东西」', async () => {
  61. register('empty')
  62. const section = await fetchPipeNetwork({ config: mockConfig })
  63. assert.equal(section.state, SECTION_STATE.EMPTY)
  64. })
  65. await check('F6 store 派生视图可直接喂给 modeLayerPlan', async () => {
  66. register('success')
  67. const store = usePipeNetworkStore()
  68. await store.load({ config: mockConfig })
  69. assert.ok(Array.isArray(store.pipelines), 'pipelines 必须是数组(计划要 .slice())')
  70. assert.ok(Array.isArray(store.facilities), 'facilities 必须是数组')
  71. assert.equal(store.pipelines.length, 6)
  72. assert.equal(store.facilities.length, 5)
  73. assert.equal(store.blocked.length, 3)
  74. })
  75. await check('F7 切行业只裁剪、不重取(图层即时切换)', async () => {
  76. register('success')
  77. const store = usePipeNetworkStore()
  78. await store.load({ config: mockConfig })
  79. store.setIndustry('gas')
  80. assert.ok(store.pipelines.every((row) => row.industry === 'GAS'))
  81. assert.ok(store.facilities.every((row) => row.industry === 'GAS'))
  82. store.setIndustry('all')
  83. assert.equal(store.pipelines.length, 6, '切回全域恢复全部')
  84. })
  85. await check('F8 接进模式计划后:管线图层 ready,不再是 degraded', async () => {
  86. register('success')
  87. const store = usePipeNetworkStore()
  88. await store.load({ config: mockConfig })
  89. store.setIndustry('all')
  90. const plan = planModeLayers({
  91. mode: 'infrastructure',
  92. entered: true,
  93. objects: [],
  94. facilities: store.facilities,
  95. pipelines: store.pipelines,
  96. })
  97. assert.equal(plan.layerStates.pipelines, 'ready')
  98. assert.equal(plan.layerStates.facilities, 'ready')
  99. assert.ok(!plan.degradedNotes.some((note) => note.key === 'pipelines-unready'), '不应再有「管线数据未就绪」')
  100. })
  101. await check('F9 取数失败时如实降级,绝不伪造空图层', async () => {
  102. const store = usePipeNetworkStore()
  103. store.configure({ config: { dataSource: 'real', api: { baseUrl: '' } } })
  104. await store.load()
  105. assert.equal(store.state, SECTION_STATE.UNAVAILABLE)
  106. assert.equal(store.pipelines, null, '失败必须交 null:交 [] 会被计划当成"已就绪但没有管线",那是谎报')
  107. const plan = planModeLayers({ mode: 'infrastructure', entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  108. assert.equal(plan.layerStates.pipelines, 'degraded', '取不到就是 degraded,不是 ready')
  109. assert.ok(plan.degradedNotes.some((note) => note.key === 'pipelines-unready'), '必须如实说「管线数据未就绪」')
  110. })
  111. await check('G1 设施与管线并入「无地图位置」清单,带中文原因', async () => {
  112. register('success')
  113. const store = usePipeNetworkStore()
  114. await store.load({ config: mockConfig })
  115. const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered))
  116. const facilityEntries = overview.blockedEntries.filter((row) => row.key.startsWith('facility|'))
  117. assert.equal(facilityEntries.length, 3, '未校验管段 + 零坐标管段 + 缺经度构件都必须在清单里')
  118. assert.ok(facilityEntries.every((row) => row.reasonText), '每条都要有中文原因')
  119. assert.ok(facilityEntries.every((row) => row.code), '每条都要有身份')
  120. })
  121. await check('G2 徽标计数与清单条数自洽(不会出现徽标 3、清单 5)', async () => {
  122. register('success')
  123. const store = usePipeNetworkStore()
  124. await store.load({ config: mockConfig })
  125. const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered))
  126. assert.equal(overview.loaded.blocked, 3)
  127. assert.equal(overview.blockedEntries.length, overview.loaded.blocked, '徽标数必须等于清单条数')
  128. assert.equal(overview.loaded.renderable, 11, '可上图 = 管段 6 + 构件 5')
  129. assert.equal(overview.loaded.total, 14)
  130. })
  131. await check('G3 几何取数失败时不得变成「零个没上图的」', async () => {
  132. const store = usePipeNetworkStore()
  133. store.configure({ config: { dataSource: 'real', api: { baseUrl: '' } } })
  134. await store.load()
  135. const overview = coordinateOverview([], null, facilityCoordinateExtra(store.filtered))
  136. assert.equal(overview.loaded.blocked, 0, '失败既不贡献可上图也不贡献不上图——不谎报')
  137. assert.equal(overview.loaded.total, 0)
  138. })
  139. await check('G4 不传 extra 时行为与票 07 原口径完全一致', () => {
  140. const objects = [
  141. { kind: 'monitoring-point', code: 'P-1', coordinate: { validated: true, renderable: true } },
  142. { kind: 'monitoring-point', code: 'P-2', coordinate: { validated: false, renderable: false } },
  143. ]
  144. const overview = coordinateOverview(objects, null)
  145. assert.equal(overview.loaded.total, 2)
  146. assert.equal(overview.loaded.renderable, 1)
  147. assert.equal(overview.loaded.blocked, 1)
  148. assert.equal(overview.blockedEntries.length, 1)
  149. })
  150. await check('H1 四个一级模式下管线图层显隐正确(管线随模式常驻)', async () => {
  151. register('success')
  152. const store = usePipeNetworkStore()
  153. await store.load({ config: mockConfig })
  154. for (const mode of ['overview', 'infrastructure', 'monitoring', 'warningResponse']) {
  155. const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  156. assert.equal(plan.layerStates.pipelines, 'ready', `${mode}:管线应可见`)
  157. assert.equal(plan.pipelines.length, 6, `${mode}:管线数量应一致`)
  158. }
  159. })
  160. await check('H2 设施图层只在总体概览/基础设施可见,切到别的模式即隐', async () => {
  161. register('success')
  162. const store = usePipeNetworkStore()
  163. await store.load({ config: mockConfig })
  164. for (const mode of ['overview', 'infrastructure']) {
  165. const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  166. assert.equal(plan.layerStates.facilities, 'ready', `${mode}:设施应可见`)
  167. assert.ok(plan.facilities.length > 0, `${mode}:设施应有内容`)
  168. }
  169. for (const mode of ['monitoring', 'warningResponse']) {
  170. const plan = planModeLayers({ mode, entered: true, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  171. assert.equal(plan.layerStates.facilities, 'hidden', `${mode}:设施应隐藏`)
  172. assert.equal(plan.facilities.length, 0, `${mode}:隐藏时不得留下残留对象`)
  173. }
  174. })
  175. await check('H3 引导态(未进入系统)图层全部隐起', async () => {
  176. register('success')
  177. const store = usePipeNetworkStore()
  178. await store.load({ config: mockConfig })
  179. const plan = planModeLayers({ mode: 'overview', entered: false, objects: [], facilities: store.facilities, pipelines: store.pipelines })
  180. assert.equal(plan.layerStates.facilities, 'hidden')
  181. assert.equal(plan.layerStates.pipelines, 'hidden')
  182. assert.equal(plan.pipelines.length, 0)
  183. })
  184. await check('H4 认不出的模式回落到总体概览,不产生未知图层态', () => {
  185. const plan = planModeLayers({ mode: '不存在的模式', entered: true, objects: [], facilities: [{ code: 'C1' }], pipelines: [{ code: 'P1' }] })
  186. assert.equal(plan.mode, 'overview')
  187. assert.equal(plan.layerStates.facilities, 'ready')
  188. assert.equal(plan.layerStates.pipelines, 'ready')
  189. })
  190. console.log(`T24 链路: ${failed ? 'FAILED' : 'passed'}`)
  191. process.exitCode = failed ? 1 : 0