Files
AgentLab/scripts/check-stream.mjs
T
ThearchyHelios c1c13fad41 助手流组件:一份 Step,宽屏和 360px 窄栏共用
AssistantStream 是纯展示的,不连 store。这样画布右栏和问数据页能用同一个
组件,而不是"一个组件三种停靠模式"——后者要求同一个实例出现在三个 DOM
父节点里,React 里除非 portal 否则不成立,硬做就变成跨路由 remount,
输入草稿和展开状态全丢。差别只在 dense 一个开关。

渲染真实事件时发现的几个问题,顺手一起修了:

- 取数运行的最终成果是一个**被截断的双重转义 JSON**(后端按字符数硬切,
  切点落在 JSON 中间是常态),严格 JSON.parse 必然失败,于是这个产品最
  典型的一次运行,成果区呈现为满屏 \"attribute01\", \"attribute02\"。
  parseQueryResult 改为解析失败时截到最后一条完整记录再解析——78 列 1 行
  的表格,比一屏转义文本有用得多。并区分"查询撞了行数上限"和"预览被切断":
  前者该去调查询,后者该去看完整工件,混成一句"已截断"等于没说。

- 审批恢复过的运行有"开始执行"+"继续执行"两条生命周期,run.finished 只
  收了第一条(find 而非 forEach),"继续执行"永远转圈——明明已经跑完。

- 处理完的审批保留着 warn 级别,一直是橙色,看着像还有事没办。

- 一屏 0ms:那些纯赋值节点确实不花时间,把没信息量的数字摆出来反而把真正
  慢的那一步淹了。低于 10ms 不显示,同时空耗时给 undefined 而不是空串。

- output 里 {"result": ""} 很常见(图跑通但出口没接上),会渲染出一条什么
  都没有的分隔线。

验证:新增 scripts/check-stream.mjs,真浏览器跑真组件——5 类真实运行 ×
(无报错/无转义 JSON/无 0ms/不横向溢出)+ 展开看 SQL 和结果表 + 空态。
check-decode.mjs 补 11 条,两个脚本共 68 条断言全过。frontend/preview.html
是离线预览页,只有 dev server 加载(已确认不进 vite build 产物)。
2026-09-17 17:00:50 +08:00

81 lines
3.5 KiB
JavaScript

// AssistantStream 的渲染回归检查。
//
// 解码器的检查(check-decode.mjs)守的是"翻译对不对",这里守的是"画出来
// 对不对"——两者能各自通过而合起来是坏的:Step 里字段都对,组件却把它渲染成
// 一屏转义 JSON,或者在 360px 窄栏里把页面撑得横向滚动。
//
// 用真浏览器跑真组件,数据还是 fixtures.json 里那批真实事件。
// 跑之前前端得起着:./scripts/dev.sh
import { chromium } from '../frontend/node_modules/playwright-core/index.mjs'
const WEB = process.env.AGENTLAB_WEB ?? 'http://localhost:5273'
// 不用 playwright install:它既下不动也会动到已有缓存。系统 Chrome 就够了。
const CHROME = process.env.CHROME_PATH
?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
const CASES = ['db', 'think', 'human', 'issue', 'failed']
let failed = 0
const check = (name, cond, detail = '') => {
console.log(` ${cond ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`)
if (!cond) failed++
}
const browser = await chromium.launch({ executablePath: CHROME })
for (const kind of CASES) {
console.log(`\n=== ${kind} ===`)
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
const errors = []
page.on('pageerror', (e) => errors.push(e.message))
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()) })
await page.goto(`${WEB}/preview.html?case=${kind}`, { waitUntil: 'networkidle' })
await page.waitForTimeout(300)
const text = await page.locator('body').innerText()
// 404 是 favicon,和组件无关
const real = errors.filter((e) => !e.includes('404'))
check('没有运行时报错', real.length === 0, real.join(' | '))
check('有内容渲染出来', text.length > 40, `${text.length} 字`)
check('没有把转义 JSON 直接吐出来', !text.includes('\\"columns\\"'))
check('没有 0ms 噪音', !/\b0ms\b/.test(text))
check('没有裸事件名', !/\b(node|run|llm|tool)\.(started|finished|start|end)\b/.test(text))
// 360px 窄栏是画布右栏的真实宽度。这里溢出,右栏就会横向滚动
const overflow = await page.evaluate(() => {
const el = document.documentElement
return el.scrollWidth - el.clientWidth
})
check('页面不横向溢出', overflow <= 0, `${overflow}px`)
await page.close()
}
console.log('\n=== 展开交互 ===')
{
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
await page.goto(`${WEB}/preview.html?case=db`, { waitUntil: 'networkidle' })
await page.waitForTimeout(300)
const before = await page.locator('body').innerText()
check('收起时不显示 SQL', !before.includes('SELECT'))
await page.locator('button', { hasText: '在 bi 上查询数据' }).first().click()
await page.waitForTimeout(250)
const after = await page.locator('body').innerText()
check('展开后能看到 SQL 原文', after.includes('SELECT') && after.includes('ANALYTICS'))
check('展开后能看到结果表', after.includes('factory_code'))
await page.close()
}
console.log('\n=== 空态 ===')
{
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
await page.goto(`${WEB}/preview.html?case=__none__`, { waitUntil: 'networkidle' })
await page.waitForTimeout(250)
const text = await page.locator('body').innerText()
check('没有内容时给的是空态而不是白屏', text.includes('问点什么'))
await page.close()
}
await browser.close()
console.log(failed ? `\n✗ ${failed} 项未通过` : '\n✓ 助手流渲染全部通过')
process.exit(failed ? 1 : 0)