| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- /**
- * 前端(Node 侧)契约冒烟:用与浏览器**同一条数据通道**(网关同源 + 相对路径)跑一遍冻契约端点。
- *
- * 为什么要它:Vue 页面在浏览器里执行,CI/命令行里没有浏览器;本脚本用 Node 24 自带的 fetch
- * 走同一网关地址与同一批路径,把"前端能不能按契约取到后端与算法服务的数据"变成可命令行复核的事实。
- *
- * 用法:
- * node app_frontEnd/web/tools/contract_smoke.mjs # 默认 http://127.0.0.1:28084
- * node app_frontEnd/web/tools/contract_smoke.mjs http://127.0.0.1:28084 /web
- * 退出码:0 全部与契约一致 · 9 有不一致 · 2 契约文件读不到
- */
- import { readFileSync } from 'node:fs'
- import { fileURLToPath } from 'node:url'
- import { dirname, resolve } from 'node:path'
- const HERE = dirname(fileURLToPath(import.meta.url))
- const CONTRACT = resolve(HERE, '../../../app_backEnd/app_backEnd_guanlan/contract/http_api_v1.json')
- const GATEWAY = process.argv[2] || 'http://127.0.0.1:28084'
- const WEB_PREFIX = process.argv[3] || '/web'
- let contract
- try {
- contract = JSON.parse(readFileSync(CONTRACT, 'utf8'))
- } catch (e) {
- console.error('[X] 读不到契约正本', CONTRACT, e.message)
- process.exit(2)
- }
- const KIND_BASE = {
- gateway: GATEWAY,
- frontend: GATEWAY,
- cms: GATEWAY,
- detail_api: GATEWAY + '/detail',
- detail_page: GATEWAY + '/detail',
- algorithm: GATEWAY + '/algorithm',
- }
- let bad = 0
- let n = 0
- console.log(`== 前端契约冒烟(Node fetch · 网关 ${GATEWAY})==`)
- for (const e of contract.endpoints) {
- const url = (KIND_BASE[e.kind] || GATEWAY) + e.path
- n += 1
- let status = 0
- let body = ''
- const t0 = Date.now()
- try {
- const r = await fetch(url, { method: e.method, headers: { Accept: '*/*' } })
- status = r.status
- body = await r.text()
- } catch (err) {
- status = 0
- body = String(err && err.message)
- }
- const ms = Date.now() - t0
- let why = ''
- if (status !== e.status) why = `状态 ${status} ≠ ${e.status}`
- else if (e.json_keys_any) {
- try {
- const o = JSON.parse(body)
- const okSet = e.json_keys_any.some((set) => set.every((k) => o && typeof o === 'object' && k in o))
- if (!okSet) why = '响应键集不在契约允许的任一种里'
- } catch {
- why = '响应不是 JSON'
- }
- } else if (e.json_keys) {
- try {
- const o = JSON.parse(body)
- const miss = e.json_keys.filter((k) => !(o && typeof o === 'object' && k in o))
- if (miss.length) why = '缺顶层键 ' + miss.slice(0, 5).join(',')
- } catch {
- why = '响应不是 JSON'
- }
- } else if (e.html_title) {
- const m = body.match(/<title>(.*?)<\/title>/s)
- const title = m ? m[1].trim() : ''
- if (title !== e.html_title) why = `标题 ${title.slice(0, 24)} ≠ ${e.html_title.slice(0, 24)}`
- }
- if (why) {
- bad += 1
- console.log(` [X] ${e.method} ${e.path} → ${why} (${ms} ms)`)
- }
- }
- // Vue 前端自身的可访问性(经网关 /web 前缀)
- try {
- const r = await fetch(GATEWAY + WEB_PREFIX + '/index.html')
- const text = await r.text()
- const ok = r.status === 200 && text.includes('id="app"')
- console.log(` [${ok ? 'OK' : 'X '}] Vue 前端 ${WEB_PREFIX}/index.html → ${r.status} · 挂载点 ${text.includes('id="app"')}`)
- if (!ok) bad += 1
- } catch (e) {
- console.log(' [X] Vue 前端不可达:', e.message)
- bad += 1
- }
- console.log(`\n契约端点 ${n} 条 · 不一致 ${bad} 条`)
- console.log('结论:', bad === 0 ? '前端数据通道与契约一致' : '存在不一致(见上)')
- process.exit(bad === 0 ? 0 : 9)
|