t18-real-audit.mjs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import fs from 'node:fs/promises'
  2. import path from 'node:path'
  3. import { fetchMapSnapshot } from '../../../src/modules/cockpit/services/mapApi.js'
  4. import { fetchBaseArchives } from '../../../src/modules/cockpit/services/infrastructureApi.js'
  5. import { fetchCurrentEvents } from '../../../src/modules/cockpit/services/eventApi.js'
  6. const base = 'http://192.168.110.96:8301/api/cockpit/v1'
  7. const output = path.resolve('.scratch/cockpit-real-data-wayfinding/verify/artifacts/t18')
  8. const observations = []
  9. const checks = []
  10. async function get(route) {
  11. const started = Date.now()
  12. try {
  13. const response = await fetch(base + route, { signal: AbortSignal.timeout(15000) })
  14. const body = await response.json()
  15. observations.push({ route, http: response.status, code: body.code, ms: Date.now() - started,
  16. readOnly: body.data?.readOnly, keys: Object.keys(body.data || {}),
  17. error: body.code !== 200 ? String(body.msg || '').slice(0, 450) : undefined,
  18. counts: body.data?.counts, totals: body.data?.totals, pagination: body.data?.pagination,
  19. mapFilter: body.data?.mapFilter, freshness: body.data?.freshness, state: body.data?.state,
  20. sectionStates: Object.fromEntries(Object.entries(body.data?.sections || {}).map(([key, value]) => [key, value?.state])),
  21. canonicalCode: body.data?.deviceCode || body.data?.pointCode,
  22. workOrders: Array.isArray(body.data?.workOrders) ? body.data.workOrders.map(row => ({ code: row.workOrderCode, stateCode: row.stateCode, associatedWithCurrentEvent: row.associatedWithCurrentEvent })) : undefined,
  23. })
  24. return { http: response.status, ...body }
  25. } catch (error) {
  26. observations.push({ route, error: String(error.message) })
  27. return { code: -1 }
  28. }
  29. }
  30. function check(id, passed, evidence) { checks.push({ id, status: passed ? 'passed' : 'failed', evidence }) }
  31. const snapshot = await get('/map-snapshot?pageSize=1000')
  32. const points = snapshot.data?.monitoringPoints || []
  33. const devices = snapshot.data?.unassociatedEquipment || []
  34. check('map-total', snapshot.data?.total === points.length + devices.length, { total: snapshot.data?.total, points: points.length, unassociated: devices.length })
  35. check('map-eight-status-buckets', snapshot.data?.pointStatusDistribution?.length === 8, { actual: snapshot.data?.pointStatusDistribution?.length ?? 'missing' })
  36. check('no-unverified-rendering', [...points, ...devices].every(row => row.coordinateValidation?.state === 'VALID' || row.coordinateValidation?.renderable === false), { blocked: [...points, ...devices].filter(row => !row.coordinateValidation?.renderable).length })
  37. const first = await get('/map-snapshot?page=1&pageSize=2')
  38. const second = await get('/map-snapshot?page=2&pageSize=2&snapshotAt=' + encodeURIComponent(first.data?.snapshotAt || ''))
  39. check('direct-snapshot-page-two', second.code === 200 && first.data?.snapshotAt === second.data?.snapshotAt && first.data?.total === second.data?.total, { first: first.code, second: second.code, total: first.data?.total })
  40. const missing = await get('/map-snapshot?page=2&pageSize=2')
  41. check('missing-token-rejected', missing.http === 400 && missing.data?.category === 'SNAPSHOT_EXPIRED', { http: missing.http, category: missing.data?.category })
  42. for (const district of ['0', '1', '2', 'UNASSIGNED']) {
  43. const result = await get('/map-snapshot?pageSize=1000&district=' + district)
  44. check('district-' + district, result.code === 200 && result.data?.mapFilter?.districtFilter === (district === 'UNASSIGNED' ? 'UNASSIGNED' : 'ASSIGNED'), { total: result.data?.total, echo: result.data?.mapFilter })
  45. }
  46. for (const industry of ['GAS', 'WATER_SUPPLY', 'DRAINAGE', 'MANHOLE']) {
  47. for (const route of ['/map-snapshot?pageSize=1000&industry=', '/monitoring/overview?industry=', '/monitoring/distribution?pageSize=1000&industry=', '/infrastructure/base-archives?industry=', '/infrastructure/management-information?industry=', '/overview/monitoring?industry=']) {
  48. const result = await get(route + industry)
  49. check(route + industry, result.code === 200 && result.data?.readOnly === true, { http: result.http, code: result.code })
  50. }
  51. }
  52. for (const status of ['ALARM', 'WARNING', 'WORK_ORDER', 'NORMAL', 'OFFLINE', 'EMPTY', 'UNKNOWN', 'UNASSOCIATED', 'ALARM,OFFLINE']) {
  53. const result = await get('/map-snapshot?pageSize=1000&pointStatus=' + status)
  54. check('status-' + status, result.code === 200 && result.data?.mapFilter?.pointStatus != null, { total: result.data?.total, echo: result.data?.mapFilter?.pointStatus ?? 'missing' })
  55. }
  56. for (const route of ['/map-snapshot?district=9', '/map-snapshot?industry=BOGUS', '/map-snapshot?pointStatus=BOGUS', '/map-snapshot?page=0', '/map-snapshot?pageSize=1001', '/monitoring/distribution?pointStatus=UNKNOWN', '/monitoring/distribution?pointStatus=UNASSOCIATED', '/events/current?mode=BOGUS']) {
  57. const result = await get(route)
  58. check('invalid-' + route, route.includes('UNKNOWN') || route.includes('UNASSOCIATED') ? result.code === 200 : result.http === 400, { http: result.http, code: result.code, category: result.data?.category })
  59. }
  60. for (const point of points) {
  61. const result = await get('/monitoring-points/' + encodeURIComponent(point.pointCode) + '?snapshotAt=' + encodeURIComponent(snapshot.data.snapshotAt))
  62. check('point-' + point.pointCode, result.code === 200, { code: result.code, deviceCodes: result.data?.devices?.map(row => row.deviceCode), state: result.data?.pointStatus })
  63. }
  64. const orderCodes = new Set()
  65. for (const device of devices) {
  66. for (const suffix of ['archive', 'metrics/current', 'metrics/history', 'events/current', 'events/history', 'work-orders', 'detail-aggregate', 'freshness']) {
  67. const window = suffix.endsWith('/history') ? '?startTime=' + encodeURIComponent('2026-10-03T00:00:00+08:00') + '&endTime=' + encodeURIComponent('2026-10-10T23:59:59+08:00') : ''
  68. const result = await get('/equipments/' + encodeURIComponent(device.deviceCode) + '/' + suffix + window)
  69. check('device-' + device.deviceCode + '-' + suffix, result.code === 200, { http: result.http, code: result.code, state: result.data?.state })
  70. for (const order of Array.isArray(result.data?.workOrders) ? result.data.workOrders : []) if (order.workOrderCode) orderCodes.add(order.workOrderCode)
  71. }
  72. }
  73. for (const code of orderCodes) {
  74. const result = await get('/work-orders/' + encodeURIComponent(code) + '/timeline')
  75. check('timeline-' + code, result.code === 200, { code: result.code })
  76. }
  77. for (const route of ['/monitoring/overview', '/monitoring/distribution', '/monitoring/situation']) {
  78. const result = await get(route)
  79. check('monitoring-createdAt-' + route, result.code === 200 && Object.prototype.hasOwnProperty.call(result.data || {}, 'createdAt'), { code: result.code, createdAt: result.data?.createdAt ?? null })
  80. }
  81. const config = { dataSource: 'real', api: { baseUrl: base } }
  82. const rawArchive = await get('/infrastructure/base-archives')
  83. const adaptedArchive = await fetchBaseArchives({ config })
  84. check('frontend-envelope-archive', adaptedArchive.state === 'success' && adaptedArchive.data?.facilityCount === rawArchive.data?.totals?.facilityCount, { state: adaptedArchive.state, adapted: adaptedArchive.data?.facilityCount, raw: rawArchive.data?.totals?.facilityCount })
  85. const adaptedMap = await fetchMapSnapshot({ config, pageSize: 1000 })
  86. check('frontend-envelope-map', adaptedMap.data?.total === snapshot.data?.total && !!adaptedMap.data?.snapshotAt, { adaptedTotal: adaptedMap.data?.total, rawTotal: snapshot.data?.total, snapshot: adaptedMap.data?.snapshotAt })
  87. const rawEvents = await get('/events/current?mode=ALARM')
  88. const adaptedEvents = await fetchCurrentEvents({ config, mode: 'ALARM' })
  89. check('frontend-business-error-not-success', rawEvents.code === 200 || ['failed', 'unavailable'].includes(adaptedEvents.state), { rawCode: rawEvents.code, adaptedState: adaptedEvents.state })
  90. const report = { timestamp: new Date().toISOString(), base, readOnly: true, observations, checks,
  91. summary: { requests: observations.length, passed: checks.filter(row => row.status === 'passed').length, failed: checks.filter(row => row.status === 'failed').length },
  92. scope: 'Live GET contracts and frontend real adapters; no database writes, no mock responses, no human verdict.' }
  93. await fs.mkdir(output, { recursive: true })
  94. await fs.writeFile(path.join(output, 'live-audit-results.json'), JSON.stringify(report, null, 2))
  95. console.log(JSON.stringify(report.summary))
  96. for (const row of checks.filter(row => row.status === 'failed')) console.log('FAIL ' + row.id + ' ' + JSON.stringify(row.evidence))
  97. process.exitCode = report.summary.failed ? 1 : 0