mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:01:59 +00:00
429398a156
1. 中文输入法选词的回车被当成发送。问数据页输入框不判组字状态(Composer 判
了,这里没有):用户每选一个词,半句问题就被送去建图、跑图。同样的漏洞还在
会话重命名、知识库搜索、{{ }} 补全、⌘Enter 运行上。收成 ui.isComposing,六处
统一——Safari 结束组字那一下 isComposing 已经是 false、keyCode 还是 229,两个
都看。
2. 「在画布里打开」后 ⌘S 覆盖上一次打开的工作流。它只调了 setGraph:换掉画布
上的节点,store 里的 workflow 还是上一张图,保存就把那张图整张覆盖了(旧版本
还在版本表里,界面上没有入口找回);没打开过图时 ⌘S 则什么都不做。现在建成
一张新工作流(以这一轮的问题命名)、走它自己的地址,原来的图不受影响。
3. 问数据页的「停止」不停。它只关了自己这头的事件流,从不调 runs.cancel,而后端
断开订阅不会取消运行——模型和 SQL 其实还在跑、还在计费,界面上却已经显示
"已取消"。等人工审批的那一轮照旧不动。
4. Copilot 编出一个不存在的节点类型,整站白屏、没保存的编辑一起丢。三层都补:
- 后端 _apply_op 拒收未知类型,不进图、不转发,最终校验里说明跳过了哪一步;
- 前端流式加节点时再拦一道;sourceHandles、属性面板遇到未知类型不再崩;
- 全站加页面级 ErrorBoundary:导航栏留着,换一页自动恢复;编辑中的图在 store
里,点「重试」时它还在。
新增 scripts/check-guards.mjs(真浏览器,拦下所有非 GET 请求,不写库不花钱):
组字回车与 keyCode 229 不发送、普通回车照常发送、未知类型不白屏、渲染真出错时
只坏这一页且换页恢复,连跑三次全绿。等待一律等事件,只有"不该发生"的断言用
窗口。后端补了未知类型不进图的用例,413 → 414 passed;tsc 通过,decode / stream
/ canvas-layout 三个检查不回归。
94 lines
4.9 KiB
JavaScript
94 lines
4.9 KiB
JavaScript
// 几道"坏了就是整页事故"的护栏:输入法回车、未知节点类型、页面级错误边界。
|
|
//
|
|
// 这三样出事时的样子都很重:中文用户每选一个词就把半句话发去建图、跑图;
|
|
// Copilot 编出一个不存在的节点类型,整站白屏、没保存的编辑一起丢;任何组件渲染
|
|
// 抛错,React 卸掉整棵树,连导航栏都没了。而 decode / stream / ui 三层检查对它们
|
|
// 一个字都不会说——它们喂的都是合法数据。
|
|
//
|
|
// 探针拦掉所有非 GET 的 /api 请求:不写库、不调模型、不花钱,随时可以跑。
|
|
// 往画布 store 里注入节点走的是 dev 构建挂在 window.__studio 上的那一份。
|
|
// 跑之前前端得起着:./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'
|
|
|
|
let failed = 0
|
|
const check = (name, cond, detail = '') => {
|
|
console.log(` ${cond ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
if (!cond) failed++
|
|
}
|
|
|
|
const browser = await chromium.launch({ executablePath: CHROME })
|
|
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } })
|
|
const writes = []
|
|
await page.route('**/api/**', (route) => {
|
|
const r = route.request()
|
|
if (r.method() === 'GET') return route.continue()
|
|
writes.push(`${r.method()} ${new URL(r.url()).pathname}`)
|
|
return route.abort()
|
|
})
|
|
const errors = []
|
|
page.on('pageerror', (e) => errors.push(e.message))
|
|
|
|
// 等到页面上出现某段文字为止。只在"应该发生"的断言上用;"不应该发生"的
|
|
// 只能等一个窗口——那是它的本性,不是偷懒
|
|
const shows = (text) => page.getByText(text).first().waitFor({ timeout: 8000 }).then(() => true, () => false)
|
|
|
|
console.log('=== 问数据页:输入法组字时的回车 ===')
|
|
await page.goto(`${WEB}/chat`, { waitUntil: 'networkidle' })
|
|
const box = page.locator('textarea').first()
|
|
await box.fill('帮我查一下本周')
|
|
const fire = (init) => box.evaluate((el, init) => el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init })), init)
|
|
writes.length = 0
|
|
await fire({ key: 'Enter', isComposing: true })
|
|
await page.waitForTimeout(400)
|
|
check('isComposing 的回车不发送', writes.length === 0, writes.join(', '))
|
|
await fire({ key: 'Enter', keyCode: 229 })
|
|
await page.waitForTimeout(400)
|
|
check('Safari 式 keyCode=229 的回车不发送', writes.length === 0, writes.join(', '))
|
|
check('草稿还在输入框里', (await box.inputValue()) === '帮我查一下本周')
|
|
const sent = page.waitForRequest((r) => r.method() !== 'GET' && r.url().includes('/api/'), { timeout: 8000 })
|
|
.then(() => true, () => false)
|
|
await fire({ key: 'Enter' })
|
|
await sent
|
|
check('普通回车照常发送(请求被探针拦下)', writes.length > 0, writes.join(', '))
|
|
|
|
console.log('\n=== 画布:未知节点类型不再白屏 ===')
|
|
await page.goto(`${WEB}/studio`, { waitUntil: 'networkidle' })
|
|
// 画布会自己跳到第一张图,等它真的把节点装进 store 再注入
|
|
await page.waitForFunction(() => window.__studio?.getState().nodes.length > 0, null, { timeout: 8000 })
|
|
await page.evaluate(() => {
|
|
const s = window.__studio.getState()
|
|
window.__studio.setState({ nodes: [...s.nodes, { id: 'ghost', type: 'card', position: { x: 60, y: 60 },
|
|
data: { nodeType: 'no_such_type', label: '幽灵节点', config: {} } }] })
|
|
window.__studio.getState().select('ghost')
|
|
})
|
|
check('属性面板说清楚是未知类型', await shows('不认识的节点类型'))
|
|
check('画布还在(没有白屏)', (await page.locator('body').innerText()).includes('幽灵节点'))
|
|
check('没有未捕获的运行时错误', errors.length === 0, errors[0] ?? '')
|
|
|
|
console.log('\n=== 错误边界:渲染真出错时只坏这一页 ===')
|
|
errors.length = 0
|
|
await page.evaluate(() => {
|
|
const s = window.__studio.getState()
|
|
window.__studio.setState({ nodes: [...s.nodes, { id: 'broken', type: 'card', position: { x: 0, y: 0 }, data: null }] })
|
|
})
|
|
check('出错时给出错误页而不是白屏', await shows('这一页出错了'))
|
|
const after = await page.locator('body').innerText()
|
|
check('导航栏还在', after.includes('问数据') && after.includes('编排'))
|
|
await page.evaluate(() => {
|
|
const s = window.__studio.getState()
|
|
window.__studio.setState({ nodes: s.nodes.filter((n) => n.id !== 'broken' && n.id !== 'ghost') })
|
|
})
|
|
await page.getByText('问数据', { exact: true }).first().click()
|
|
await page.waitForURL(/\/chat/)
|
|
check('换一页就恢复', await page.getByText('这一页出错了').waitFor({ state: 'detached', timeout: 8000 })
|
|
.then(() => true, () => false))
|
|
|
|
await browser.close()
|
|
console.log(failed ? `\n✗ ${failed} 项未通过` : '\n✓ 护栏全部通过')
|
|
process.exit(failed ? 1 : 0)
|