feat(host): /window 路由与 float_window 工具共用同一段动作逻辑;smoke-host 升级为 20 条断言的映射/路由测试(真实抓包形状),实测 open 能拉起浮窗
This commit is contained in:
+87
-31
@@ -1,51 +1,107 @@
|
||||
/**
|
||||
* 冒烟测试:用假的 ctx 调 apply(),确认插件不会抛异常,路由能应答。
|
||||
* 冒烟 + 单元测试:用假的 ctx 调 apply(),然后
|
||||
* 1) 断言路由/SSE 应答正常;
|
||||
* 2) 喂进**真实宿主里抓到的**会话事件(0.1.x / 0.1.7 两种形状),断言镜像映射与指标;
|
||||
* 3) 断言 Agent 工具注册成功。
|
||||
* 用法:node tools/smoke-host.mjs
|
||||
*/
|
||||
import { apply, name, inject } from '../lib/host.js'
|
||||
|
||||
const routes = []
|
||||
const events = []
|
||||
const effects = []
|
||||
|
||||
const ctx = {
|
||||
logger: { info: (...a) => console.log('[mock]', ...a) },
|
||||
webServer: {
|
||||
port: 3099,
|
||||
host: '127.0.0.1',
|
||||
register: (r) => { routes.push(r); return () => { } },
|
||||
},
|
||||
get: () => undefined,
|
||||
set: () => { },
|
||||
on: (ev, fn) => { events.push([ev, fn]); return () => { } },
|
||||
effect: (fn) => { const d = fn(); effects.push(d); return () => { } },
|
||||
let failed = 0
|
||||
function ok(cond, label, extra = '') {
|
||||
if (cond) { console.log(' PASS', label) } else { failed++; console.log(' FAIL', label, extra) }
|
||||
}
|
||||
function eq(actual, expected, label) {
|
||||
const a = JSON.stringify(actual), e = JSON.stringify(expected)
|
||||
ok(a === e, label, `expected ${e} got ${a}`)
|
||||
}
|
||||
|
||||
console.log('name =', name, '| inject =', JSON.stringify(inject))
|
||||
// ---------------------------------------------------------------- 假 ctx
|
||||
const routes = []
|
||||
const listeners = []
|
||||
const tools = []
|
||||
const sessions = []
|
||||
const ctx = {
|
||||
logger: { info: () => { } },
|
||||
webServer: { port: 3099, host: '127.0.0.1', register: (r) => { routes.push(r); return () => { } } },
|
||||
get: (k) => (k === 'tools' ? { register: (t) => tools.push(t) } : k === 'sessions' ? { list: () => sessions } : undefined),
|
||||
provide: () => { }, set: () => { },
|
||||
on: (ev, fn) => { listeners.push([ev, fn]); return () => { } },
|
||||
effect: (fn) => { const d = fn(); return () => { } },
|
||||
}
|
||||
|
||||
console.log(`plugin: ${name} inject=${JSON.stringify(inject)}`)
|
||||
apply(ctx)
|
||||
console.log('routes =', routes.map((r) => `${r.kind}:${r.path}`).join(', '))
|
||||
console.log('events =', events.map((e) => e[0]).join(', '))
|
||||
console.log('effects =', effects.length)
|
||||
|
||||
console.log('routes:', routes.map((r) => `${r.kind}:${r.path}`).join(', '))
|
||||
console.log('tools:', tools.map((t) => t.name).join(', '))
|
||||
|
||||
function fakeRes() {
|
||||
return {
|
||||
status: 0, headers: null, body: '',
|
||||
writeHead(s, h) { this.status = s; this.headers = h },
|
||||
write(chunk) { this.body += chunk; return true },
|
||||
end(chunk) { if (chunk) this.body += chunk },
|
||||
write(c) { this.body += c; return true },
|
||||
end(c) { if (c) this.body += c },
|
||||
}
|
||||
}
|
||||
|
||||
const route = routes[0]
|
||||
for (const p of ['/dsh-float-window/health', '/dsh-float-window/state', '/dsh-float-window/nope']) {
|
||||
async function call(path, method = 'GET', body = null) {
|
||||
const res = fakeRes()
|
||||
await route.handler({ method: 'GET', url: p, on: () => { } }, res)
|
||||
console.log(p, '->', res.status, String(res.body).slice(0, 120))
|
||||
const req = {
|
||||
method, url: path, on: () => { },
|
||||
async *[Symbol.asyncIterator]() { if (body) yield Buffer.from(JSON.stringify(body)) },
|
||||
}
|
||||
await route.handler(req, res)
|
||||
return res
|
||||
}
|
||||
|
||||
// SSE 分支:只验证不抛异常且写出了 data:
|
||||
const res = fakeRes()
|
||||
await route.handler({ method: 'GET', url: '/dsh-float-window/events', on: () => { } }, res)
|
||||
console.log('events ->', res.status, res.headers['Content-Type'], JSON.stringify(res.body.slice(0, 80)))
|
||||
// ---------------------------------------------------------------- 事件喂入
|
||||
const session = { id: 's-1', meta: { title: null } }
|
||||
sessions.push(session)
|
||||
const fire = (type, data, extra = {}) => listeners.forEach(([ev, fn]) => ev === 'session/event' && fn(session, { type, data, ...extra }))
|
||||
|
||||
console.log('SMOKE OK')
|
||||
console.log('\n[1] 事件映射(真实抓包形状)')
|
||||
fire('user/message', { content: [{ type: 'text', text: '你好' }], role: 'user', source: { kind: 'user' }, id: 'm1' })
|
||||
fire('user/message', { content: [{ type: 'text', text: 'SYSTEM PROMPT SNAPSHOT ...' }], role: 'user', source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' } })
|
||||
fire('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text', text: '你' } })
|
||||
fire('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text', text: '好' } })
|
||||
fire('assistant/message', { content: [{ type: 'text', text: '你好!' }] })
|
||||
fire('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'finish', reason: { kind: 'error', failure: { message: 'boom' } } } })
|
||||
fire('tool/call', { name: 'read', args: { path: 'a.txt' } })
|
||||
fire('session/title', { title: '测试会话' })
|
||||
fire('turn/end', {})
|
||||
fire('step/end', {})
|
||||
|
||||
const st = await call('/dsh-float-window/state')
|
||||
const snap = JSON.parse(st.body)
|
||||
console.log(' messages =', JSON.stringify(snap.messages))
|
||||
eq(st.status, 200, 'GET /state 200')
|
||||
eq(snap.title, '测试会话', '会话标题进镜像')
|
||||
eq(snap.messages.length, 5, '消息行数(系统提示被过滤、chunk 合并、错误行、tool 行)')
|
||||
eq(snap.messages[0], { role: 'user', text: '你好' }, 'user/message -> 用户行')
|
||||
eq(snap.messages[1], { role: 'assistant', text: '你好', streaming: true }, 'assistant/chunk 流式合并')
|
||||
eq(snap.messages[2].role, 'assistant', 'assistant/message -> 助手行')
|
||||
ok(snap.messages[3].text.startsWith('⚠️'), 'finish+error chunk -> 错误行')
|
||||
eq(snap.messages[4].role, 'tool', 'tool/call -> 工具行')
|
||||
ok(!JSON.stringify(snap.messages).includes('SYSTEM PROMPT'), '插件注入的系统提示被过滤')
|
||||
eq(snap.metrics.turns, 1, 'turn/end 计数')
|
||||
eq(snap.metrics.steps, 1, 'step/end 计数')
|
||||
|
||||
console.log('\n[2] 路由与 SSE')
|
||||
eq((await call('/dsh-float-window/health')).status, 200, 'GET /health 200')
|
||||
eq((await call('/dsh-float-window/nope')).status, 404, '未知路径 404')
|
||||
const sse = await call('/dsh-float-window/events')
|
||||
eq(sse.status, 200, 'GET /events 200')
|
||||
ok(String(sse.headers['Content-Type']).includes('text/event-stream'), 'SSE content-type')
|
||||
ok(sse.body.startsWith(': connected'), 'SSE 先发注释帧')
|
||||
ok(sse.body.includes('"sessionId":"s-1"'), 'SSE 首帧带第一个快照')
|
||||
|
||||
console.log('\n[3] /window 与 /chat 的错误路径(不该抛异常)')
|
||||
const w = await call('/dsh-float-window/window', 'POST', { action: 'status' })
|
||||
eq(w.status, 200, 'POST /window(status) 200')
|
||||
ok(JSON.parse(w.body).text.includes('alive='), '/window 返回状态文本')
|
||||
const c = await call('/dsh-float-window/chat', 'POST', { text: 'hi' })
|
||||
ok(JSON.parse(c.body).ok === false, '没有宿主会话 API 时 /chat 结构化失败而不是抛错')
|
||||
|
||||
console.log(failed === 0 ? '\nSMOKE OK' : `\nSMOKE FAILED (${failed})`)
|
||||
process.exit(failed === 0 ? 0 : 1)
|
||||
|
||||
Reference in New Issue
Block a user