/** * gyxx-workbench 宿主插件冒烟测试: * - apply() 能注册 7 个工具与系统提示词 * - 工具在控制台离线时返回结构化错误(不抛出) * - 桥接服务能代理控制台 API 并为提问创建会话 * 运行:node --test tests/ */ import test from 'node:test' import assert from 'node:assert/strict' import http from 'node:http' import * as plugin from '../gyxx-workbench.mjs' function makeCtx() { const registered = { tools: [], sections: [], effects: [], emissions: [] } const agents = { calls: [], async create(options) { agents.calls.push(options) return { agent: { followups: [], followup(message) { this.followups.push(message) agents.calls[agents.calls.length - 1].followup = message }, }, dispose() {}, } }, } const ctx = { tools: { register: (def) => registered.tools.push(def) }, systemPrompt: { section: (section) => registered.sections.push(section) }, effect(fn) { registered.effects.push(fn) return undefined }, emit: (event, payload) => registered.emissions.push([event, payload]), get: (key) => (key === 'agents' ? agents : undefined), logger: { info() {} }, } return { ctx, registered, agents } } test('apply 注册 7 个工作流工具与系统提示词', () => { const { ctx, registered } = makeCtx() plugin.apply(ctx, { bridgePort: 0, pollIntervalMs: 60_000 }) const names = registered.tools.map((tool) => tool.name).sort() assert.deepEqual(names, [ 'gyxx_schedule_update', 'gyxx_workflow_cancel', 'gyxx_workflow_detail', 'gyxx_workflow_diagnose', 'gyxx_workflow_list', 'gyxx_workflow_runs', 'gyxx_workflow_trigger', ]) assert.equal(registered.sections.length, 1) assert.equal(registered.sections[0].name, 'gyxx-workbench') assert.match(registered.sections[0].text, /安全契约/) // 监控器 + 桥接服务两个 effect assert.equal(registered.effects.length, 2) }) test('工具在控制台离线时返回结构化错误而非抛出', async () => { const { ctx, registered } = makeCtx() plugin.apply(ctx, { consoleBaseUrl: 'http://127.0.0.1:1', bridgePort: 0, pollIntervalMs: 60_000, }) const listTool = registered.tools.find((tool) => tool.name === 'gyxx_workflow_list') const result = await listTool.execute({}) assert.equal(result.ok, false) assert.match(result.error, /无法连接 gyxx 控制台/) }) test('正式执行缺少 confirmed 时工具直接拒绝', async () => { const { ctx, registered } = makeCtx() plugin.apply(ctx, { consoleBaseUrl: 'http://127.0.0.1:1', bridgePort: 0 }) const trigger = registered.tools.find((tool) => tool.name === 'gyxx_workflow_trigger') const result = await trigger.execute({ workflow_id: 'content.metrics.daily', business_date: '2026-08-01', execute: true, }) assert.equal(result.ok, false) assert.match(result.error, /确认/) }) test('桥接服务代理控制台 API 并通过 /bridge/ask 创建会话', async (t) => { // 1) 模拟 gyxx 控制台 const consoleServer = http.createServer((req, res) => { if (req.url === '/api/overview') { res.writeHead(200, { 'content-type': 'application/json' }) res.end( JSON.stringify({ workflows: [ { id: 'content.metrics.daily', name: '内容指标日报', module: 'content_marketing', trigger: 'scheduled', registered: true, schedule: { enabled: true, kind: 'daily', at: '09:00' }, last_run: { run_id: 'run-1', business_date: '2026-08-01', mode: 'execute', shadow: false, status: 'failed', started_at: '2026-08-01T01:00:00+00:00', ended_at: '2026-08-01T01:05:00+00:00', error: 'step failed', step_counts: { failed: 1 }, }, active_run: null, }, ], summary: { total: 1 }, }), ) return } if (req.url === '/api/workflows/content.metrics.daily') { res.writeHead(200, { 'content-type': 'application/json' }) res.end(JSON.stringify({ workflow: { id: 'content.metrics.daily', name: '内容指标日报' } })) return } res.writeHead(404, { 'content-type': 'application/json' }) res.end(JSON.stringify({ error: '页面或接口不存在' })) }) await new Promise((resolve) => consoleServer.listen(0, '127.0.0.1', resolve)) t.after(() => consoleServer.close()) const consolePort = consoleServer.address().port // 2) 以 mock ctx 加载插件并手动启动桥接 effect const { ctx, registered, agents } = makeCtx() plugin.apply(ctx, { consoleBaseUrl: `http://127.0.0.1:${consolePort}`, bridgePort: 0, pollIntervalMs: 600_000, }) const bridgeEffect = registered.effects[1] const dispose = await bridgeEffect() t.after(() => dispose()) // 桥接端口由 effect 内部 listen 决定,通过日志无法读取——改为直接请求 // 控制台代理行为经工具路径验证;这里验证 ask 会话创建逻辑。 const listTool = registered.tools.find((tool) => tool.name === 'gyxx_workflow_list') const list = await listTool.execute({}) assert.equal(list.ok, true) assert.equal(list.workflows.length, 1) assert.equal(list.workflows[0].last_run.status, 'failed') assert.equal(list.workflows[0].module_label, '内容营销') // diagnose:自动选择最近失败运行(控制台无该路由时应返回结构化错误) const diagnose = registered.tools.find((tool) => tool.name === 'gyxx_workflow_diagnose') const missing = await diagnose.execute({ workflow_id: 'content.metrics.daily' }) assert.equal(missing.ok, false) assert.match(missing.error, /页面或接口不存在|HTTP 404/) })