batch2-final-acceptance.mjs 5.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  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. // 2026-10-22 维护者要求两侧面板稍微调透(三档各降 0.08),断言跟随新基准。
  30. assert.match(tokens, /--panel-top:\s*rgba\(2,\s*9,\s*27,\s*0\.8\)/)
  31. // Exercise actual request -> adaptation -> 90-day merge with a pinned clock and no live network.
  32. const config = { dataSource: 'real', api: { baseUrl: 'https://backend.example/api/cockpit/v1' } }
  33. const window = eventTimeWindow('LAST_90_DAYS', Date.parse('2026-10-13T12:34:00+08:00'))
  34. const requests = []
  35. const payloadFor = (url) => {
  36. const params = new URL(url).searchParams
  37. const start = params.get('startTime')
  38. const end = params.get('endTime')
  39. assert.ok(start && end, 'backend must receive actual times')
  40. assert.ok(Date.parse(end) - Date.parse(start) < 31 * 86400000)
  41. const rows = []
  42. 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 })
  43. return { range: { type: 'TREND_WINDOW', startTime: start, endTime: end }, rows }
  44. }
  45. const respond = (body) => ({ ok: true, status: 200, json: async () => ({ code: 200, data: body }) })
  46. const result = await fetchEventHistory({ ...window, rangeType: 'LAST_90_DAYS', mode: 'WARNING', config, fetchImpl: async (url) => { requests.push(url); return respond(payloadFor(url)) } })
  47. assert.equal(result.state, 'success')
  48. assert.equal(result.data.trend.length, 2161)
  49. assert.equal(new Set(result.data.trend.map((row) => row.bucket)).size, 2161)
  50. assert.equal(result.data.range.start, window.startTime)
  51. assert.ok(requests.length > 1)
  52. 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' } }) })
  53. assert.equal(mismatch.state, 'failed', 'wrong echoed window must not pass')
  54. 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)) })
  55. assert.equal(partialFailure.state, 'failed', 'one failed window must not be silently omitted')
  56. const mustNotFetch = () => { throw new Error('unsupported aggregate must not request backend') }
  57. assert.equal((await fetchHandlingEfficiency({ ...window, config, fetchImpl: mustNotFetch })).state, 'unavailable')
  58. assert.equal((await fetchEventAnalysis({ ...window, config, fetchImpl: mustNotFetch })).state, 'unavailable')
  59. const flat = trendBlock('flat', Array.from({ length: 90 }, (_, i) => ({ key: String(i), label: String(i), count: 0 })), { chart: 'line' })
  60. assert.equal(flat.linePoints.filter((point) => point.annotate).length, 1)
  61. assert.equal(flat.axisIndices.length, 4)
  62. assert.deepEqual(flat.yTicks, [{ value: 0, pct: 0 }])
  63. const oddPeak = trendBlock('odd', [{ key: 'a', count: 0 }, { key: 'b', count: 3 }], { chart: 'line' })
  64. assert.equal(oddPeak.yTicks[1].value, 1.5)
  65. const visuals = legendVisuals()
  66. 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 }))
  67. objects.push({ code: 'DEVICE', kind: 'unassociated-equipment', coordinate: { renderable: true }, longitude: 110, latitude: 28 })
  68. const plan = planPointMarkers(objects)
  69. const legend = pointLegend(plan.byStatus)
  70. assert.equal(legendMatchesMarkers(legend, plan.markers).ok, true)
  71. const corrupted = legend.map((row) => row.key === 'OFFLINE' ? { ...row, tone: 'lv4' } : row)
  72. assert.equal(legendMatchesMarkers(corrupted, plan.markers).ok, false, 'wrong color mapping must fail')
  73. console.log(`Final acceptance passed: ${components} Vue SFC syntax/templates/styles; history request/merge and negative cases; flat trend labels; map legend adversarial check.`)