Files
dsh-float-window/tools/smoke-host.mjs
T

108 lines
5.1 KiB
JavaScript

/**
* 冒烟 + 单元测试:用假的 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'
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}`)
}
// ---------------------------------------------------------------- 假 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('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(c) { this.body += c; return true },
end(c) { if (c) this.body += c },
}
}
const route = routes[0]
async function call(path, method = 'GET', body = null) {
const res = fakeRes()
const req = {
method, url: path, on: () => { },
async *[Symbol.asyncIterator]() { if (body) yield Buffer.from(JSON.stringify(body)) },
}
await route.handler(req, res)
return res
}
// ---------------------------------------------------------------- 事件喂入
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('\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)