contract_smoke.mjs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /**
  2. * 前端(Node 侧)契约冒烟:用与浏览器**同一条数据通道**(网关同源 + 相对路径)跑一遍冻契约端点。
  3. *
  4. * 为什么要它:Vue 页面在浏览器里执行,CI/命令行里没有浏览器;本脚本用 Node 24 自带的 fetch
  5. * 走同一网关地址与同一批路径,把"前端能不能按契约取到后端与算法服务的数据"变成可命令行复核的事实。
  6. *
  7. * 用法:
  8. * node app_frontEnd/web/tools/contract_smoke.mjs # 默认 http://127.0.0.1:28084
  9. * node app_frontEnd/web/tools/contract_smoke.mjs http://127.0.0.1:28084 /web
  10. * 退出码:0 全部与契约一致 · 9 有不一致 · 2 契约文件读不到
  11. */
  12. import { readFileSync } from 'node:fs'
  13. import { fileURLToPath } from 'node:url'
  14. import { dirname, resolve } from 'node:path'
  15. const HERE = dirname(fileURLToPath(import.meta.url))
  16. const CONTRACT = resolve(HERE, '../../../app_backEnd/app_backEnd_guanlan/contract/http_api_v1.json')
  17. const GATEWAY = process.argv[2] || 'http://127.0.0.1:28084'
  18. const WEB_PREFIX = process.argv[3] || '/web'
  19. let contract
  20. try {
  21. contract = JSON.parse(readFileSync(CONTRACT, 'utf8'))
  22. } catch (e) {
  23. console.error('[X] 读不到契约正本', CONTRACT, e.message)
  24. process.exit(2)
  25. }
  26. const KIND_BASE = {
  27. gateway: GATEWAY,
  28. frontend: GATEWAY,
  29. cms: GATEWAY,
  30. detail_api: GATEWAY + '/detail',
  31. detail_page: GATEWAY + '/detail',
  32. algorithm: GATEWAY + '/algorithm',
  33. }
  34. let bad = 0
  35. let n = 0
  36. console.log(`== 前端契约冒烟(Node fetch · 网关 ${GATEWAY})==`)
  37. for (const e of contract.endpoints) {
  38. const url = (KIND_BASE[e.kind] || GATEWAY) + e.path
  39. n += 1
  40. let status = 0
  41. let body = ''
  42. const t0 = Date.now()
  43. try {
  44. const r = await fetch(url, { method: e.method, headers: { Accept: '*/*' } })
  45. status = r.status
  46. body = await r.text()
  47. } catch (err) {
  48. status = 0
  49. body = String(err && err.message)
  50. }
  51. const ms = Date.now() - t0
  52. let why = ''
  53. if (status !== e.status) why = `状态 ${status} ≠ ${e.status}`
  54. else if (e.json_keys_any) {
  55. try {
  56. const o = JSON.parse(body)
  57. const okSet = e.json_keys_any.some((set) => set.every((k) => o && typeof o === 'object' && k in o))
  58. if (!okSet) why = '响应键集不在契约允许的任一种里'
  59. } catch {
  60. why = '响应不是 JSON'
  61. }
  62. } else if (e.json_keys) {
  63. try {
  64. const o = JSON.parse(body)
  65. const miss = e.json_keys.filter((k) => !(o && typeof o === 'object' && k in o))
  66. if (miss.length) why = '缺顶层键 ' + miss.slice(0, 5).join(',')
  67. } catch {
  68. why = '响应不是 JSON'
  69. }
  70. } else if (e.html_title) {
  71. const m = body.match(/<title>(.*?)<\/title>/s)
  72. const title = m ? m[1].trim() : ''
  73. if (title !== e.html_title) why = `标题 ${title.slice(0, 24)} ≠ ${e.html_title.slice(0, 24)}`
  74. }
  75. if (why) {
  76. bad += 1
  77. console.log(` [X] ${e.method} ${e.path} → ${why} (${ms} ms)`)
  78. }
  79. }
  80. // Vue 前端自身的可访问性(经网关 /web 前缀)
  81. try {
  82. const r = await fetch(GATEWAY + WEB_PREFIX + '/index.html')
  83. const text = await r.text()
  84. const ok = r.status === 200 && text.includes('id="app"')
  85. console.log(` [${ok ? 'OK' : 'X '}] Vue 前端 ${WEB_PREFIX}/index.html → ${r.status} · 挂载点 ${text.includes('id="app"')}`)
  86. if (!ok) bad += 1
  87. } catch (e) {
  88. console.log(' [X] Vue 前端不可达:', e.message)
  89. bad += 1
  90. }
  91. console.log(`\n契约端点 ${n} 条 · 不一致 ${bad} 条`)
  92. console.log('结论:', bad === 0 ? '前端数据通道与契约一致' : '存在不一致(见上)')
  93. process.exit(bad === 0 ? 0 : 9)