import assert from 'node:assert/strict'; import { EventEmitter } from 'node:events'; import test from 'node:test'; import { loadConfig } from '../src/config.js'; import { AgentBusListener, type AgentBusSocket, type AgentBusTaskGateway, createTaskResultFrame, createTaskProgressFrame, extractAgentBusBusinessText, isInboundAgentBusTask, parseAgentBusFrame, sessionFromReadyFrame, taskAttachmentReferences, taskResultStatus, taskResultText } from '../src/agentbus.js'; import { AgentBusManager, mergeRuntimeChannelStatuses, type PublicAgentBusChannel } from '../src/agentbus-channels.js'; import { resolveBusinessRoute } from '../src/business-routes.js'; import type { AgentBusDelivery, PublicTask } from '../src/task-service.js'; function makeTask(status: string, overrides: Partial = {}): PublicTask { return { organization_id: 'org-1', task_id: 'TASK-1', source: 'manual', channel_id: null, channel_name: null, raw_text: '测试指令', conversation_id: 'conversation-1', agent_session: { status: 'active', turn_no: 1, recovery_count: 0, session_id_present: false }, parser: { route_id: null, input_contract_version: null, configured_mode: 'ai', config_revision: 0, authoritative_engine: null, engine_affinity: 'ai', version: null, fallback_reason: null, comparison_status: null, decision_id: null, review_status: null, diff_paths: [], field_differences: [], unreviewed_difference_count: 0, shadow_differences: [], reparse_count: 0 }, input_request: null, important_message: null, operation: null, parse_response: null, result: null, summary: null, status, stage: 'parse', message: '任务状态已更新。', error: '', failure: null, success_receipt: null, error_summary: null, handoff_status: '', confirmation_mode: 'manual', confirmed_at: null, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), last_event_id: null, events: [], ...overrides }; } class FakeSocket { readyState = 0; readonly sent: Record[] = []; private readonly handlers = new Map void>>(); on(event: string, listener: (...args: any[]) => void): this { const listeners = this.handlers.get(event) || []; listeners.push(listener); this.handlers.set(event, listeners); return this; } emit(event: string, ...args: any[]): void { for (const listener of this.handlers.get(event) || []) listener(...args); } send(data: string, callback?: (error?: Error) => void): void { this.sent.push(JSON.parse(data) as Record); callback?.(); } close(): void { this.readyState = 3; this.emit('close', 1000, Buffer.alloc(0)); } } async function waitFor(predicate: () => boolean, timeoutMs = 1_000): Promise { const deadline = Date.now() + timeoutMs; while (!predicate() && Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 5)); } } function testConfig() { return loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 4).toString('base64'), AGENTBUS_ENABLED: 'true', AGENTBUS_WS_URL: 'wss://mesh.nianxx.cn/ws', AGENTBUS_WS_TOKEN: 'test-ws-token', AGENTBUS_BOT_ADDRESS: 'bot:test:listener', AGENTBUS_WS_RECONNECT_DELAY: '1ms', AGENTBUS_TASK_TIMEOUT_MS: '1000', AGENTBUS_LOG_PAYLOADS: 'true' }); } function publicChannelFixture(overrides: Partial = {}): PublicAgentBusChannel { return { id: 'channel-1', organization_id: 'org-1', channel_type: 'agentbus', display_name: '外部用户 A', external_user_ref: 'external-user-a', agentbus_bot_address: 'bot:test:listener', enabled: true, status: 'disabled', key_configured: true, deletable: true, last_connected_at: null, last_error: null, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), ...overrides }; } test('AgentBus configuration stays disabled until connection fields are supplied', () => { const config = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 5).toString('base64') }); assert.equal(config.agentBusEnabled, false); assert.equal(config.AGENTBUS_WS_RECONNECT_DELAY, 5_000); assert.equal(config.AGENTBUS_LOG_PAYLOADS, false); }); test('AgentBus auto configuration rejects partial credentials', () => { assert.throws(() => loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 6).toString('base64'), AGENTBUS_WS_TOKEN: 'partial-token' }), /AGENTBUS_WS_URL/); }); test('database-managed channels may use the global WebSocket URL without a global key', () => { const config = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 7).toString('base64'), AGENTBUS_ENABLED: 'true', AGENTBUS_WS_URL: 'wss://mesh.nianxx.cn/ws' }); assert.equal(config.agentBusEnabled, true); assert.equal(config.AGENTBUS_WS_TOKEN, undefined); }); test('WeChat transport envelope exposes only its business text to the global parser', () => { const businessText = [ '独立团单个下单', '发团日期:2026-11-02', '预订客户:示例客户', '产品搜索:示例产品', '预估人数:15+1', '用房数量:TWN 8,SGL 1' ].join('\n'); const wrapped = [ 'New WeChat message', 'Conversation: conversation-example-1', `Text: ${businessText}` ].join('\n'); const extracted = extractAgentBusBusinessText(wrapped); assert.equal(extracted, businessText); assert.equal(resolveBusinessRoute(extracted).routeId, 'team_order_create'); const envelopeConversation = parseAgentBusFrame(JSON.stringify({ id: 'wechat-envelope-conversation', type: 'event', from: 'channel:wechat:user-1', payload: { text: wrapped } })); assert.equal(envelopeConversation?.conversation_id, 'conversation-example-1'); const explicitConversation = parseAgentBusFrame(JSON.stringify({ id: 'wechat-explicit-conversation', type: 'event', from: 'channel:wechat:user-1', conversation_id: 'conversation-explicit-1', payload: { text: wrapped } })); assert.equal(explicitConversation?.conversation_id, 'conversation-explicit-1'); const missingConversation = `New WeChat message\nConversation:\nText: ${businessText}`; const wrongTextLabel = `New WeChat message\nConversation: conversation-example-1\nBody: ${businessText}`; const emptyBody = 'New WeChat message\nConversation: conversation-example-1\nText: '; assert.equal(extractAgentBusBusinessText(missingConversation), missingConversation); assert.equal(extractAgentBusBusinessText(wrongTextLabel), wrongTextLabel); assert.equal(extractAgentBusBusinessText(emptyBody), emptyBody.trim()); }); test('channel directory prefers the current listener over stale persisted disabled status', () => { const channel = publicChannelFixture({ status: 'disabled', last_error: '旧进程状态' }); const connected = mergeRuntimeChannelStatuses([channel], [{ channel_id: channel.id, enabled: true, connected: true, session_ready: true }]); assert.equal(connected[0].status, 'connected'); assert.equal(connected[0].last_error, null); const connecting = mergeRuntimeChannelStatuses([channel], [{ channel_id: channel.id, enabled: true, connected: false, session_ready: false }]); assert.equal(connecting[0].status, 'connecting'); const disabled = mergeRuntimeChannelStatuses( [publicChannelFixture({ enabled: false, status: 'disabled' })], [{ channel_id: channel.id, enabled: true, connected: true, session_ready: true }] ); assert.equal(disabled[0].status, 'disabled'); }); test('AgentBus protocol helpers preserve reply routing fields', () => { const inbound = parseAgentBusFrame(JSON.stringify({ id: 'channel-event-1', type: 'event', from: 'channel:wechat:user-1', conversation_id: 'conversation-1', payload: { text: '你好', reply_policy: { progress: true } } })); assert.ok(inbound); assert.equal(isInboundAgentBusTask(inbound), true); const session = { id: 'session-1', epoch: 3, address: 'bot:test:listener' }; const ready = sessionFromReadyFrame(parseAgentBusFrame(JSON.stringify({ id: 'ready-1', type: 'event', session_id: session.id, epoch: session.epoch, to: session.address, payload: { event: 'session.ready' } }))!); assert.deepEqual(ready, session); const progress = createTaskProgressFrame(inbound, session, '处理中', 'progress-1'); assert.equal(progress.reply_to, 'channel-event-1'); assert.equal(progress.conversation_id, 'conversation-1'); assert.equal((progress.payload as Record).event, 'task.progress'); const result = createTaskResultFrame(inbound, session, 'completed', '已完成', 'result-1'); assert.equal(result.from, session.address); assert.equal(result.to, inbound.from); assert.equal(result.session_id, session.id); assert.equal(result.epoch, session.epoch); assert.equal(result.reply_to, inbound.id); assert.equal((result.payload as Record).event, 'task.result'); const important = { kind: 'awaiting_user_input' as const, text: '请补充房型分配。', recorded_at: new Date().toISOString() }; const resultWithImportantMessage = createTaskResultFrame( inbound, session, 'completed', important.text, 'result-important-1', important ); assert.deepEqual((resultWithImportantMessage.payload as Record).important_message, { kind: 'awaiting_user_input', text: '请补充房型分配。' }); const detailedSuccess = { kind: 'success' as const, text: '创建完成。\n团号:LW-260907A-B', recorded_at: new Date().toISOString(), success_receipt: { recorded_at: new Date().toISOString(), receipt: { group_number: 'LW-260907A-B', verification_status: 'split_parent_completed' } } }; const channelResult = createTaskResultFrame( inbound, session, 'completed', '创建完成。\n团号:LW-260907A-B', 'result-channel-summary-1', detailedSuccess ); assert.deepEqual((channelResult.payload as Record).important_message, { kind: 'success', text: '创建完成。\n团号:LW-260907A-B' }); assert.doesNotMatch(JSON.stringify(channelResult.payload), /success_receipt|verification_status|split_parent_completed/); const directFileResult = createTaskResultFrame( inbound, session, 'completed', '团队文件已准备', 'result-file-1', detailedSuccess, [{ id: 'artifact-direct-1', name: 'team.docx', content_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', size: 3, inline: false, url: 'https://files.example.test/team.docx', sha256: 'a'.repeat(64) }] ); assert.deepEqual((directFileResult.payload as Record).attachments, [{ id: 'artifact-direct-1', name: 'team.docx', content_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', size: 3, inline: false, url: 'https://files.example.test/team.docx', sha256: 'a'.repeat(64) }]); const insecureFileResult = createTaskResultFrame( inbound, session, 'completed', '团队文件已准备', 'result-insecure-file-1', { kind: 'success', text: '团队文件已准备', recorded_at: new Date().toISOString(), attachments: [{ name: 'team.docx', download_url: 'http://127.0.0.1:8786/file.docx' }] } ); assert.equal((insecureFileResult.payload as Record).attachments, undefined); const senderOnly = parseAgentBusFrame(JSON.stringify({ id: 'channel-event-without-conversation-1', type: 'event', from: 'channel:wechat:user-2', payload: { text: '补充信息' } })); assert.ok(senderOnly); const fallbackProgress = createTaskProgressFrame(senderOnly, session, '处理中', 'progress-fallback-1'); assert.equal(fallbackProgress.conversation_id, 'agentbus:channel:wechat:user-2'); }); test('AgentBus result text uses the unified important message and preserves the confirmation gate', () => { const needsInput = makeTask('awaiting_user_input', { input_request: { missing_fields: ['data.room_counts'], questions: [{ field: 'data.room_counts', prompt: '请补充房型分配。' }], captured_facts: {} }, important_message: { kind: 'awaiting_user_input', text: '请补充房型分配,并说明是否一单一房。', recorded_at: new Date().toISOString(), input_request: { missing_fields: ['data.room_counts'], questions: [{ field: 'data.room_counts', prompt: '请补充房型分配。' }], captured_facts: {} } } }); assert.match(taskResultText(needsInput), /是否一单一房/); assert.equal(taskResultStatus(needsInput), 'completed'); const completed = makeTask('completed', { message: '散拼-创建母团计划已提交并完成回查。', important_message: { kind: 'success', text: '散拼-创建母团计划已提交并完成回查。\n团号:LW-260907A-B', recorded_at: new Date().toISOString(), success_receipt: { recorded_at: new Date().toISOString(), receipt: { group_numbers: ['LW-260907A-B', 'LW-260915A-A'], room_counts: { SGL: 1, TWN: 8 }, verification_status: 'split_parent_completed' } } } }); assert.equal( taskResultText(completed), '散拼-创建母团计划已提交并完成回查。\n团号:LW-260907A-B' ); assert.doesNotMatch(taskResultText(completed), /最终回执|verification_status|split_parent_completed/); const awaitingConfirmation = makeTask('awaiting_confirmation'); assert.equal(taskResultText(awaitingConfirmation), '已受理,正在处理。'); assert.equal(taskResultStatus(awaitingConfirmation), 'completed'); const failed = makeTask('parse_failed', { failure: { error_code: 'blocked', failure_stage: 'parse', failure_source: 'agent', failure_message: '资料不完整', agent_returned: true, plugin_dispatch_started: false, erp_write_started: false, no_plugin_dispatch: true, no_erp_write: true, validation_errors: [] } }); assert.equal(taskResultStatus(failed), 'failed'); assert.match(taskResultText(failed), /资料不完整/); const blocked = makeTask('blocked', { important_message: { kind: 'error', text: '插件执行阻断:客户候选未唯一解析,已阻断 ERP 写入。;GetProduct ajax request was not observed; product_after_effect: expected exactly one existing LTJT option, found 0; Required fields still blank: customer, product.', recorded_at: new Date().toISOString() } }); assert.equal( taskResultText(blocked), '插件执行阻断:客户候选未唯一解析,已阻断 ERP 写入。' ); assert.doesNotMatch(taskResultText(blocked), /GetProduct|product_after_effect|Required fields/); const erpFailure = makeTask('blocked', { error_summary: { recorded_at: new Date().toISOString(), error_code: 'erp_business_rule_blocked', stage: 'lifecycle_live_submit', source: 'erp', message: '此团还有【应收团款】账,不能取消!', validation_errors: [] }, important_message: { kind: 'error', text: '此团还有【应收团款】账,不能取消!', recorded_at: new Date().toISOString(), error_summary: { recorded_at: new Date().toISOString(), error_code: 'erp_business_rule_blocked', stage: 'lifecycle_live_submit', source: 'erp', message: '此团还有【应收团款】账,不能取消!', validation_errors: [] } } }); assert.equal(taskResultStatus(erpFailure), 'failed'); assert.equal(taskResultText(erpFailure), '此团还有【应收团款】账,不能取消!'); const uncertainWrite = makeTask('reconciliation_pending', { important_message: { kind: 'error', text: 'ERP 写入结果不确定:write_after_requery_mismatch', recorded_at: new Date().toISOString() } }); assert.equal(taskResultStatus(uncertainWrite), 'failed'); assert.equal( taskResultText(uncertainWrite), 'ERP 写入已发起,但系统尚未确认最终结果。请勿重复提交同一任务,等待管理员只读核验。' ); }); test('AgentBus attachment references omit archived visitor XLS and keep only XLSX delivery', () => { const task = makeTask('completed', { result: { report: { status: 'export_source_completed', artifacts: [ { artifact_id: 'artifact-visitor-xls', file_name: 'Visitor.xls', content_type: 'application/vnd.ms-excel', bytes: 9377, storage_backend: 'oss', download_url: 'https://files.example.test/Visitor.xls', agentbus_visible: false }, { artifact_id: 'artifact-visitor-xlsx', file_name: 'Visitor.xlsx', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', bytes: 5211, storage_backend: 'oss', download_url: 'https://files.example.test/Visitor.xlsx', agentbus_visible: true } ] } } }); assert.deepEqual(taskAttachmentReferences(task), [{ artifact_id: 'artifact-visitor-xlsx', name: 'Visitor.xlsx', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 5211, url: 'https://files.example.test/Visitor.xlsx', storage_backend: 'oss' }]); }); test('AgentBus listener connects with the documented Authorization header and returns one final result', async (t) => { const config = testConfig(); const socket = new FakeSocket(); let capturedUrl = ''; let capturedHeaders: Record = {}; let currentTask = makeTask('parse_queued'); const events = new EventEmitter(); const received: Array> = []; let capturedContext: Record = {}; const logs: Array<{ level: string; metadata: Record; message?: string }> = []; const tasks: AgentBusTaskGateway = { events, async ingestMessage(context, input) { capturedContext = context as unknown as Record; received.push(input as unknown as Record); return { task: currentTask, attached: false, created: true }; }, async getTask() { return currentTask; } }; const listener = new AgentBusListener({ config, tasks, organizationId: 'org-1', scheduleParseQueue: async () => { currentTask = makeTask('awaiting_confirmation'); events.emit('task', { id: 1, organization_id: 'org-1', task_id: currentTask.task_id, status: currentTask.status, stage: 'parse', message: currentTask.message, payload: {}, created_at: new Date().toISOString() }); await new Promise((resolve) => setTimeout(resolve, 10)); currentTask = makeTask('confirmed', { confirmation_mode: 'automatic', confirmed_at: new Date().toISOString(), handoff_status: 'awaiting_handoff', message: '指令解析完成,已进入全自动化 ERP 执行。' }); events.emit('task', { id: 2, organization_id: 'org-1', task_id: currentTask.task_id, status: currentTask.status, stage: 'automation', message: currentTask.message, payload: {}, created_at: new Date().toISOString() }); await new Promise((resolve) => setTimeout(resolve, 10)); currentTask = makeTask('completed', { message: '散拼-创建母团计划已提交并完成回查。', important_message: { kind: 'success', text: '散拼-创建母团计划已提交并完成回查。\n团号:LW-260907A-B', recorded_at: new Date().toISOString(), success_receipt: { recorded_at: new Date().toISOString(), receipt: { group_number: 'LW-260907A-B', verification_status: 'split_parent_completed' } } } }); events.emit('task', { id: 3, organization_id: 'org-1', task_id: currentTask.task_id, status: currentTask.status, stage: 'browser_execution', message: currentTask.message, payload: {}, created_at: new Date().toISOString() }); }, socketFactory: (url, options) => { capturedUrl = url; capturedHeaders = options.headers; return socket as unknown as AgentBusSocket; }, logger: { info(metadata, message) { logs.push({ level: 'info', metadata, message }); }, warn(metadata, message) { logs.push({ level: 'warn', metadata, message }); }, error(metadata, message) { logs.push({ level: 'error', metadata, message }); } } }); t.after(() => listener.stop()); listener.start(); assert.match(capturedUrl, /[?&]ready=1/); assert.equal(capturedHeaders.Authorization, 'Bearer test-ws-token'); socket.readyState = 1; socket.emit('open'); socket.emit('message', JSON.stringify({ id: 'ready-1', type: 'event', session_id: 'session-1', epoch: 1, to: 'bot:test:listener', payload: { event: 'session.ready' } })); socket.emit('message', JSON.stringify({ id: 'ignored-frame-1', type: 'message', from: 'channel:wechat:user-1', payload: { text: '格式不符合监听契约' } })); const wrappedBusinessText = [ 'New WeChat message', 'Conversation: conversation-wechat-1', 'Text: 独立团单个下单', '发团日期:2026-11-02', '预订客户:示例客户', '产品搜索:示例产品', '预估人数:15+1', '用房数量:TWN 8,SGL 1' ].join('\n'); socket.emit('message', JSON.stringify({ id: 'channel-event-1', type: 'event', from: 'channel:wechat:user-1', payload: { text: wrappedBusinessText, reply_policy: { progress: true } } })); await waitFor(() => socket.sent.length === 2); assert.equal(received.length, 1); assert.equal(capturedContext.source, 'agentbus'); assert.equal(received[0].message, wrappedBusinessText.split('\n').slice(2).join('\n').replace(/^Text:\s*/, '')); assert.equal(received[0].conversationId, 'conversation-wechat-1'); assert.equal(received[0].idempotencyKey, 'agentbus:channel-event-1'); assert.equal(socket.sent.length, 2); assert.equal((socket.sent[0].payload as Record).event, 'task.progress'); assert.equal((socket.sent[0].payload as Record).status, 'accepted'); assert.equal((socket.sent[0].payload as Record).text, '已受理,正在处理。'); assert.equal((socket.sent[1].payload as Record).event, 'task.result'); assert.equal(socket.sent[1].reply_to, 'channel-event-1'); assert.match(String((socket.sent[1].payload as Record).text), /散拼-创建母团计划已提交并完成回查/); assert.match(String((socket.sent[1].payload as Record).text), /LW-260907A-B/); assert.doesNotMatch(String((socket.sent[1].payload as Record).text), /最终回执|verification_status|split_parent_completed/); assert.deepEqual((socket.sent[1].payload as Record).important_message, { kind: 'success', text: '散拼-创建母团计划已提交并完成回查。\n团号:LW-260907A-B' }); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'listener_starting')); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'session_ready')); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_ignored' && entry.metadata.ignore_reason === 'unexpected_type:message')); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_received' && entry.metadata.frame_id === 'channel-event-1')); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_received' && entry.metadata.text_preview === wrappedBusinessText)); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'task_processing_started' && entry.metadata.transport_envelope_unwrapped === true)); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'task_ingested' && entry.metadata.task_id === 'TASK-1')); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'outbound_progress_sent')); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'outbound_result_sent')); assert.doesNotMatch(JSON.stringify(logs), /test-ws-token/); }); test('WeChat attachment placeholder without file metadata fails closed before task ingestion', async (t) => { const socket = new FakeSocket(); const events = new EventEmitter(); const logs: Array<{ level: string; metadata: Record; message?: string }> = []; let ingestCalls = 0; const tasks: AgentBusTaskGateway = { events, async ingestMessage() { ingestCalls += 1; return { task: makeTask('parse_queued'), attached: false, created: true }; }, async getTask() { return makeTask('failed'); } }; const listener = new AgentBusListener({ config: testConfig(), tasks, organizationId: 'org-1', scheduleParseQueue: async () => {}, socketFactory: () => socket as unknown as AgentBusSocket, logger: { info(metadata, message) { logs.push({ level: 'info', metadata, message }); }, warn(metadata, message) { logs.push({ level: 'warn', metadata, message }); }, error(metadata, message) { logs.push({ level: 'error', metadata, message }); } } }); t.after(() => listener.stop()); listener.start(); socket.readyState = 1; socket.emit('open'); socket.emit('message', JSON.stringify({ id: 'ready-attachment-placeholder', type: 'event', session_id: 'session-attachment-placeholder', epoch: 1, to: 'bot:test:listener', payload: { event: 'session.ready' } })); socket.emit('message', JSON.stringify({ id: 'wechat-attachment-placeholder', type: 'event', from: 'channel:wechat:user-1', payload: { text: [ 'New WeChat message', 'Conversation: thread:conversation-attachment-1', 'Text: [WeChat attachment: synthetic-roster.xlsx]' ].join('\n') } })); await waitFor(() => socket.sent.length === 1); assert.equal(ingestCalls, 0); assert.equal(socket.sent.length, 1); assert.equal(socket.sent[0].conversation_id, 'thread:conversation-attachment-1'); assert.equal((socket.sent[0].payload as Record).event, 'task.result'); assert.equal((socket.sent[0].payload as Record).status, 'failed'); assert.equal( (socket.sent[0].payload as Record).text, '附件内容未传到平台,原任务仍在等待附件。请检查微信桥接器的文件转发后重新发送。' ); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'task_processing_failed' && entry.metadata.error_code === 'roster_attachment_metadata_missing' && /^[a-f0-9]{24}$/u.test(String(entry.metadata.error_fingerprint)))); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'task_processing_started' && entry.metadata.attachment_count === 0 && entry.metadata.attachment_placeholder === true)); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'attachment_metadata_missing')); assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'outbound_result_sent')); }); test('listener reload stop does not overwrite the channel status as disabled', () => { const statuses: string[] = []; const listener = new AgentBusListener({ config: testConfig(), tasks: {} as AgentBusTaskGateway, organizationId: 'org-1', scheduleParseQueue: async () => {}, onStatusChange: (status) => statuses.push(status) }); listener.stop(false); assert.deepEqual(statuses, []); listener.stop(); assert.deepEqual(statuses, ['disabled']); }); test('channel manager replays a reload requested while another reload is in flight', async () => { const manager = new AgentBusManager({ config: testConfig(), tasks: {} as AgentBusTaskGateway, organizationId: 'org-1', scheduleParseQueue: async () => {} }); const service = manager.channelService as unknown as { ensureLegacyChannel: (organizationId: string) => Promise; listEnabledSecrets: (organizationId: string) => Promise<[]>; }; let listCalls = 0; let releaseFirstList: (() => void) | undefined; service.ensureLegacyChannel = async () => {}; service.listEnabledSecrets = async () => { listCalls += 1; if (listCalls === 1) { await new Promise((resolve) => { releaseFirstList = resolve; }); } return []; }; const initialReload = manager.start(); while (!releaseFirstList) await new Promise((resolve) => setImmediate(resolve)); const mutationReload = manager.reload(); releaseFirstList(); await Promise.all([initialReload, mutationReload]); assert.equal(listCalls, 2); await manager.stop(); }); test('listener shutdown carries its session epoch for stale-writer protection', () => { const socket = new FakeSocket(); const changes: Array<{ status: string; epoch: number | null }> = []; const listener = new AgentBusListener({ config: testConfig(), tasks: {} as AgentBusTaskGateway, organizationId: 'org-1', scheduleParseQueue: async () => {}, socketFactory: () => socket as unknown as AgentBusSocket, onStatusChange: (status, _error, epoch) => changes.push({ status, epoch }) }); listener.start(); socket.readyState = 1; socket.emit('message', JSON.stringify({ id: 'ready-epoch-1', type: 'event', session_id: 'session-epoch-1', epoch: 7, to: 'bot:test:listener', payload: { event: 'session.ready' } })); listener.stop(); assert.deepEqual(changes.at(-1), { status: 'disabled', epoch: 7 }); }); test('durable channel listener persists route and resends accepted/result deliveries', async () => { const config = testConfig(); const socket = new FakeSocket(); const events = new EventEmitter(); let currentTask = makeTask('parse_queued', { task_id: 'TASK-DURABLE', channel_id: 'channel-1', channel_name: '外部用户 A' }); let acceptedCreated = false; let resultEnqueued = false; const deliveryState = { acceptedDelivered: false, resultDelivered: false, failures: [] as string[] }; let resultPayload: Record = {}; const accepted: AgentBusDelivery = { id: 'delivery-accepted', channel_id: 'channel-1', task_id: 'task-row-1', inbound_frame_id: 'channel-durable-1', inbound_from: 'channel:external:user-a', conversation_id: 'conversation-durable-1', delivery_kind: 'accepted', payload: { event: 'task.progress', status: 'accepted', text: '已受理,正在处理。' }, attempt_count: 1 }; const result: AgentBusDelivery = { ...accepted, id: 'delivery-result', delivery_kind: 'result', payload: resultPayload }; let capturedHeaders: Record = {}; let receivedContext: Record = {}; let receivedInput: Record = {}; const tasks: AgentBusTaskGateway & { deliveryState: typeof deliveryState } = { deliveryState, events, async ingestMessage(context, input) { acceptedCreated = true; receivedContext = context as unknown as Record; receivedInput = input as unknown as Record; return { task: currentTask, attached: false, created: true }; }, async getTask() { return currentTask; }, async listAgentBusFinalizationCandidates() { if (acceptedCreated && deliveryState.acceptedDelivered && !resultEnqueued && currentTask.status === 'completed') { return [{ channel_id: 'channel-1', task_id: currentTask.task_id, inbound_frame_id: accepted.inbound_frame_id }]; } return []; }, async enqueueAgentBusResult(_channelId, _inboundFrameId, payload) { resultPayload = payload; result.payload = resultPayload; resultEnqueued = true; }, async claimAgentBusDeliveries() { if (acceptedCreated && !deliveryState.acceptedDelivered) return [accepted]; if (resultEnqueued && !deliveryState.resultDelivered) return [result]; return []; }, async markAgentBusDeliveryDelivered(this: { deliveryState: typeof deliveryState }, deliveryId) { if (deliveryId === accepted.id) this.deliveryState.acceptedDelivered = true; if (deliveryId === result.id) this.deliveryState.resultDelivered = true; }, async markAgentBusDeliveryFailed(this: { deliveryState: typeof deliveryState }, _deliveryId, errorMessage) { this.deliveryState.failures.push(errorMessage); }, async releaseAgentBusDeliveries() {} }; const listener = new AgentBusListener({ config, tasks, organizationId: 'org-1', scheduleParseQueue: async () => { currentTask = makeTask('completed', { task_id: 'TASK-DURABLE', channel_id: 'channel-1', channel_name: '外部用户 A', message: '任务已完成。', result: { report: { status: 'export_source_completed', artifacts: [ { artifact_id: 'artifact-durable-source-xls', file_name: 'Visitor.xls', content_type: 'application/vnd.ms-excel', bytes: 3, sha256: 'b'.repeat(64), storage_backend: 'oss', download_url: 'https://files.example.test/Visitor.xls', agentbus_visible: false }, { artifact_id: 'artifact-durable-1', file_name: 'Visitor.xlsx', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', bytes: 4, sha256: 'a'.repeat(64), storage_backend: 'oss', download_url: 'https://files.example.test/Visitor.xlsx', agentbus_visible: true } ] } }, important_message: { kind: 'success', text: '任务已完成。', recorded_at: new Date().toISOString() } }); }, socketFactory: (_url, options) => { capturedHeaders = options.headers; return socket as unknown as AgentBusSocket; }, channel: { id: 'channel-1', displayName: '外部用户 A', wsUrl: 'wss://mesh.nianxx.cn/ws', wsToken: 'channel-ws-token', botAddress: 'bot:channel-a:listener' } }); listener.start(); assert.equal(capturedHeaders.Authorization, 'Bearer channel-ws-token'); socket.readyState = 1; socket.emit('open'); socket.emit('message', JSON.stringify({ id: 'ready-durable-1', type: 'event', session_id: 'session-durable-1', epoch: 4, to: 'bot:channel-a:listener', payload: { event: 'session.ready' } })); socket.emit('message', JSON.stringify({ id: 'channel-durable-1', type: 'event', from: 'channel:external:user-a', conversation_id: 'conversation-durable-1', payload: { text: '创建一个新团' } })); const deadline = Date.now() + 3_000; while (Date.now() < deadline && !deliveryState.resultDelivered) { await new Promise((resolve) => setTimeout(resolve, 50)); } assert.equal(receivedContext.channelId, 'channel-1'); assert.equal(receivedInput.channelId, 'channel-1'); assert.deepEqual(receivedInput.agentBusRoute, { inboundFrameId: 'channel-durable-1', inboundFrom: 'channel:external:user-a', conversationId: 'conversation-durable-1' }); assert.deepEqual(resultPayload.attachments, [{ artifact_id: 'artifact-durable-1', name: 'Visitor.xlsx', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 4, url: 'https://files.example.test/Visitor.xlsx', sha256: 'a'.repeat(64), storage_backend: 'oss' }]); assert.doesNotMatch(JSON.stringify(resultPayload), /content_base64|download_url/); assert.equal(socket.sent.length, 2); assert.equal(socket.sent[0].id, 'bot-accepted-delivery-accepted'); assert.equal((socket.sent[0].payload as Record).event, 'task.progress'); assert.equal(socket.sent[1].id, 'bot-result-delivery-result'); assert.equal(socket.sent[1].reply_to, 'channel-durable-1'); assert.equal((socket.sent[1].payload as Record).event, 'task.result'); assert.equal((socket.sent[1].payload as Record).status, 'completed'); assert.deepEqual((socket.sent[1].payload as Record).attachments, [{ id: 'artifact-durable-1', name: 'Visitor.xlsx', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 4, inline: false, url: 'https://files.example.test/Visitor.xlsx', sha256: 'a'.repeat(64) }]); assert.equal(deliveryState.acceptedDelivered, true); assert.equal(deliveryState.resultDelivered, true); assert.deepEqual(deliveryState.failures, []); listener.stop(); });