Files
group_xinghuo_jinrong/scripts/e2e/04-analyst-market-advisor.mjs
T
zhanghongyu_0626 188bf6a438 feat(analyst_agent): Add support for recent days queries and enhance SQL hint generation
- Introduced `_recent_days_series_hint` to handle queries related to "最近N天" for time series data aggregation.
- Added `_cn_num_to_int` function to convert Chinese numerals to integers for better query parsing.
- Updated `_nl_sql_hints` to incorporate the new hint generation logic, ensuring accurate SQL output for recent days queries.
- Enhanced documentation to reflect these changes and improve clarity on the new functionalities.
2026-09-13 22:36:19 +08:00

114 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { launch, instrument, login, BASE, SHOT_DIR, makeEmitter } from './harness.mjs'
const b = await launch()
const ctx = await b.newContext({ viewport: { width: 1280, height: 900 } })
const p = await ctx.newPage()
const rec = instrument(p)
const out = []
const emit = makeEmitter(out) // F-13:边跑边出,中途异常不丢已完成证据
const ok = (n, pass, d = '') => emit(`${pass ? 'PASS' : 'FAIL'} | ${n}${d ? ' | ' + d : ''}`)
const txt = async () => (await p.locator('body').innerText()).replace(/\s+/g, ' ')
// ============== ANALYST ==============
await login(p, 'analyst')
await p.goto(`${BASE}/#/app/analyst/home`, { waitUntil: 'domcontentloaded' })
await p.waitForTimeout(6000)
await p.screenshot({ path: `${SHOT_DIR}/analyst-home.png`, fullPage: true })
ok('分析员首页有内容', (await txt()).length > 200, (await txt()).slice(150, 340))
await p.goto(`${BASE}/#/app/analytics/query`, { waitUntil: 'domcontentloaded' })
await p.waitForTimeout(7000)
const qText = await txt()
ok('问数工作台渲染看板指标', /看板指标|产品数|上涨/.test(qText), qText.slice(120, 340))
await p.screenshot({ path: `${SHOT_DIR}/analyst-query-empty.png`, fullPage: true })
// ask a question
const ta = p.getByPlaceholder(/例如:我近30天有多少笔交易/)
await ta.fill('待处理预警按类型统计')
// AntD inserts a space between two CJK glyphs: the label renders as "问 数".
await p.locator('button').filter({ hasText: /问\s*数/ }).first().click()
await p.waitForTimeout(16000)
const ansText = await txt()
await p.screenshot({ path: `${SHOT_DIR}/analyst-query-answer.png`, fullPage: true })
ok('问数返回结果', /模板|缓存|结果表格|SQL/.test(ansText), ansText.slice(-420))
// F-11 修复:原断言把**两类性质完全不同的东西**塞进同一个正则判「无原始枚举泄漏」:
// ① 页面自身的**状态枚举**(template_hit / cache_hit / pending_review)—— 属 UI 文案,
// 确实不该裸露;实测该页已中文化(渲染为「未命中模板 / 未命中结果缓存」)。
// ② 查询结果的**数据值**(large_amount / freq_trade)—— 见下。
// 事实:本页的问题就是「待处理预警按类型统计」,结果表按 `alert_type` 分组,
// 列名原样显示为 `ALERT_TYPE / CNT`,页面**同时展示 SQL 供溯源**(实测 SQL: 4 行 · 112 ms)。
// ⇒ 在这张**原始结果表**里出现 `large_amount` 是**设计使然、且正确**:本地化它会破坏
// 「数据值 → 分组统计」的对应关系,也会让 SQL 与结果不一致(工作台契约 = 原始数据 + SQL)。
// 故拆开判定:状态枚举保持硬断言;数据值降级为 INFO(记录实际,**不进 FAIL 分母**)。
// 该「无原始枚举」准则本身没错,但适用范围是**交易型 UI**(如风控预警台账,
// `03-risk.mjs` 的同类断言在那边 PASS 且有意义),不适用于原始数据控制台。
const statusEnumLeak = ansText.match(/\b(template_hit|cache_hit|pending_review)\b/)
ok('问数结果无**状态**枚举泄漏(UI 文案已中文化)',
!statusEnumLeak, statusEnumLeak ? `泄漏 ${statusEnumLeak[0]}` : 'clean')
const dataValueEnum = ansText.match(/\b(large_amount|freq_trade|suitability|aml)\b/)
emit(`INFO | 问数结果表中的原始数据值 | ${dataValueEnum ? dataValueEnum[0] : '未出现'}(按设计展示原始查询结果 + SQL,数据值不本地化)`)
// ask empty question -> should not fire
await p.goto(`${BASE}/#/app/analytics/query`, { waitUntil: 'domcontentloaded' })
await p.waitForTimeout(5000)
await p.goto(`${BASE}/#/app/analytics/assets`, { waitUntil: 'domcontentloaded' })
await p.waitForTimeout(6000)
const assetsText = await txt()
await p.screenshot({ path: `${SHOT_DIR}/analyst-assets.png`, fullPage: true })
ok('资产沉淀页有内容', assetsText.length > 300, assetsText.slice(120, 360))
// ============== MARKET (shared) ==============
await p.goto(`${BASE}/#/app/market`, { waitUntil: 'domcontentloaded' })
await p.waitForTimeout(6000)
const mkText = await txt()
await p.screenshot({ path: `${SHOT_DIR}/market.png`, fullPage: true })
ok('行情页有数据行', /PROD-|净值|涨/.test(mkText), mkText.slice(120, 340))
const mktIso = mkText.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
ok('行情页无原始 ISO 时间', !mktIso, mktIso ? mktIso[0] : 'clean')
// search filter
const search = p.getByPlaceholder('按产品名/代码筛选')
if (await search.count()) {
const rowsBefore = await p.locator('.ant-table-tbody tr').count()
await search.fill('稳健')
await p.waitForTimeout(1500)
const rowsAfter = await p.locator('.ant-table-tbody tr').count()
ok('行情搜索可过滤', rowsAfter > 0 && rowsAfter < rowsBefore, `${rowsBefore} -> ${rowsAfter}`)
await search.fill('zzzz-no-such-product')
await p.waitForTimeout(1500)
const emptyAfter = await p.locator('.ant-table-placeholder').count()
ok('行情搜索无结果时不报错', emptyAfter >= 0, `placeholder=${emptyAfter}`)
} else {
ok('行情页有搜索框', false)
}
// ============== ADVISOR ==============
await p.goto(`${BASE}/#/login`, { waitUntil: 'domcontentloaded' })
await p.evaluate(() => localStorage.clear())
await p.reload({ waitUntil: 'domcontentloaded' })
await login(p, 'advisor')
await p.goto(`${BASE}/#/app/advisor/home`, { waitUntil: 'domcontentloaded' })
await p.waitForTimeout(8000)
const advHome = await txt()
await p.screenshot({ path: `${SHOT_DIR}/advisor-home.png`, fullPage: true })
ok('理财师首页有客户数/AUM', /客户|AUM|市值/.test(advHome), advHome.slice(150, 400))
await p.goto(`${BASE}/#/app/advisor/customers`, { waitUntil: 'domcontentloaded' })
await p.waitForTimeout(8000)
const advCust = await txt()
const advRows = await p.locator('.ant-table-tbody').innerText().catch(() => '')
await p.screenshot({ path: `${SHOT_DIR}/advisor-customers.png`, fullPage: true })
ok('名下客户列表有数据', advRows.trim().length > 0, advRows.replace(/\n/g, ' | ').slice(0, 240))
ok('名下客户页无原始 ISO', !/\d{4}-\d{2}-\d{2}T/.test(advRows), advRows.replace(/\n/g, ' | ').slice(0, 200))
// F-13:明细已在产生时即时输出;此处只给汇总(即使中断也不丢已完成证据)
const _nP = out.filter((l) => l.startsWith('PASS')).length
const _nF = out.filter((l) => l.startsWith('FAIL')).length
const _nI = out.filter((l) => l.startsWith('INFO')).length
console.log(`
===== ${_nP} PASS / ${_nF} FAIL / ${_nI} INFO =====`)
console.log('\n--- errors ---')
console.log(JSON.stringify({ pageErrors: [...new Set(rec.pageErrors)], httpErrors: rec.httpErrors.slice(0, 12) }))
await b.close()