batch2-final-acceptance.mjs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import assert from 'node:assert/strict'
  2. import { readFileSync, readdirSync } from 'node:fs'
  3. import { createRequire } from 'node:module'
  4. import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
  5. import { fetchEventHistory, fetchHandlingEfficiency, fetchEventAnalysis } from '../../../src/modules/cockpit/services/warningResponseApi.js'
  6. import { eventTimeWindow } from '../../../src/modules/cockpit/domain/eventRange.js'
  7. import { trendBlock } from '../../../src/modules/cockpit/domain/panelBlocks.js'
  8. import { legendMatchesMarkers, legendVisuals, planPointMarkers, pointLegend } from '../../../src/modules/cockpit/domain/pointStatus.js'
  9. const require = createRequire(import.meta.url)
  10. const postcss = require('postcss')
  11. const root = new URL('../../../', import.meta.url)
  12. let components = 0
  13. for (const name of ['CockpitShell.vue', ...readdirSync(new URL('src/modules/cockpit/components/', root)).filter((file) => file.endsWith('.vue')).map((file) => `components/${file}`)]) {
  14. const filename = `src/modules/cockpit/${name}`
  15. const source = readFileSync(new URL(filename, root), 'utf8')
  16. const result = parse(source, { filename })
  17. assert.deepEqual(result.errors, [], filename)
  18. const descriptor = result.descriptor
  19. const script = descriptor.scriptSetup ? compileScript(descriptor, { id: filename }) : null
  20. if (descriptor.template) {
  21. const template = compileTemplate({ source: descriptor.template.content, filename, id: filename, compilerOptions: { bindingMetadata: script?.bindings || {} } })
  22. assert.deepEqual(template.errors, [], filename)
  23. }
  24. for (const style of descriptor.styles) postcss.parse(style.content, { from: filename })
  25. components++
  26. }
  27. const tokens = readFileSync(new URL('src/modules/cockpit/styles/tokens.css', root), 'utf8')
  28. postcss.parse(tokens)
  29. assert.match(tokens, /--panel-top:\s*rgba\(2,\s*9,\s*27,\s*0\.88\)/)
  30. // Exercise actual request -> adaptation -> 90-day merge with a pinned clock and no live network.
  31. const config = { dataSource: 'real', api: { baseUrl: 'https://backend.example/api/cockpit/v1' } }
  32. const window = eventTimeWindow('LAST_90_DAYS', Date.parse('2026-10-13T12:34:00+08:00'))
  33. const requests = []
  34. const payloadFor = (url) => {
  35. const params = new URL(url).searchParams
  36. const start = params.get('startTime')
  37. const end = params.get('endTime')
  38. assert.ok(start && end, 'backend must receive actual times')
  39. assert.ok(Date.parse(end) - Date.parse(start) < 31 * 86400000)
  40. const rows = []
  41. for (let t = Math.floor(Date.parse(start) / 3600000) * 3600000; t <= Date.parse(end); t += 3600000) rows.push({ timeBucket: new Date(t).toISOString(), eventCount: 1 })
  42. return { range: { type: 'TREND_WINDOW', startTime: start, endTime: end }, rows }
  43. }
  44. const respond = (body) => ({ ok: true, status: 200, json: async () => ({ code: 200, data: body }) })
  45. const result = await fetchEventHistory({ ...window, rangeType: 'LAST_90_DAYS', mode: 'WARNING', config, fetchImpl: async (url) => { requests.push(url); return respond(payloadFor(url)) } })
  46. assert.equal(result.state, 'success')
  47. assert.equal(result.data.trend.length, 2161)
  48. assert.equal(new Set(result.data.trend.map((row) => row.bucket)).size, 2161)
  49. assert.equal(result.data.range.start, window.startTime)
  50. assert.ok(requests.length > 1)
  51. const mismatch = await fetchEventHistory({ ...window, rangeType: 'LAST_90_DAYS', config, fetchImpl: async (url) => respond({ ...payloadFor(url), range: { startTime: '2026-01-01T00:00:00Z', endTime: '2026-01-02T00:00:00Z' } }) })
  52. assert.equal(mismatch.state, 'failed', 'wrong echoed window must not pass')
  53. const partialFailure = await fetchEventHistory({ ...window, rangeType: 'LAST_90_DAYS', config, fetchImpl: async (url) => new URL(url).searchParams.get('startTime') === window.startTime ? { ok: false, status: 500 } : respond(payloadFor(url)) })
  54. assert.equal(partialFailure.state, 'failed', 'one failed window must not be silently omitted')
  55. const mustNotFetch = () => { throw new Error('unsupported aggregate must not request backend') }
  56. assert.equal((await fetchHandlingEfficiency({ ...window, config, fetchImpl: mustNotFetch })).state, 'unavailable')
  57. assert.equal((await fetchEventAnalysis({ ...window, config, fetchImpl: mustNotFetch })).state, 'unavailable')
  58. const flat = trendBlock('flat', Array.from({ length: 90 }, (_, i) => ({ key: String(i), label: String(i), count: 0 })), { chart: 'line' })
  59. assert.equal(flat.linePoints.filter((point) => point.annotate).length, 1)
  60. assert.equal(flat.axisIndices.length, 4)
  61. assert.deepEqual(flat.yTicks, [{ value: 0, pct: 0 }])
  62. const oddPeak = trendBlock('odd', [{ key: 'a', count: 0 }, { key: 'b', count: 3 }], { chart: 'line' })
  63. assert.equal(oddPeak.yTicks[1].value, 1.5)
  64. const visuals = legendVisuals()
  65. const objects = visuals.filter((visual) => visual.key !== 'UNASSOCIATED').map((visual) => ({ code: visual.key, kind: 'monitoring-point', status: visual.key, coordinate: { renderable: true }, longitude: 110, latitude: 28 }))
  66. objects.push({ code: 'DEVICE', kind: 'unassociated-equipment', coordinate: { renderable: true }, longitude: 110, latitude: 28 })
  67. const plan = planPointMarkers(objects)
  68. const legend = pointLegend(plan.byStatus)
  69. assert.equal(legendMatchesMarkers(legend, plan.markers).ok, true)
  70. const corrupted = legend.map((row) => row.key === 'OFFLINE' ? { ...row, tone: 'lv4' } : row)
  71. assert.equal(legendMatchesMarkers(corrupted, plan.markers).ok, false, 'wrong color mapping must fail')
  72. console.log(`Final acceptance passed: ${components} Vue SFC syntax/templates/styles; history request/merge and negative cases; flat trend labels; map legend adversarial check.`)