/** * 冒烟 + 单元测试:用假的 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)