'use strict'; const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-callback-relay-smoke-')); process.env.QIWEI_OUTPUTS_DIR = root; const agentServicePath = require.resolve('../mcp/src/dashboard/agent-service'); const bridged = []; require.cache[agentServicePath] = { id: agentServicePath, filename: agentServicePath, loaded: true, exports: { ingestWebhookMessage: async message => { bridged.push(message); return { status: 'pending_review' }; }, }, }; const { processWebhookEvents } = require('../mcp/src/core/webhook-processor'); const { readProcessedEventIds } = require('../mcp/src/core/webhook-store'); const { getGroupOperationsStore } = require('../mcp/src/core/group-operations-store'); const { writeConfirmedMapping, readRoomMessages, } = require('../mcp/src/tools/qiwei-group-management-run'); const { runPollOnce } = require('./start-relay-client'); async function main() { const messageId = `callback-private-${Date.now()}`; const result = await processWebhookEvents({ code: 0, msg: 'test', source: 'relay', data: [{ guid: 'device-1', cmd: 15000, msgType: 1, msgUniqueIdentifier: messageId, senderId: 'customer-1', senderName: '测试客户', receiverId: 'staff-1', timestamp: Math.floor(Date.now() / 1000), seq: 1, msgData: { content: '回调消息测试' }, }], }); assert.strictEqual(result.processed, 1); assert.strictEqual(result.errors, 0); assert.strictEqual(bridged.length, 1); assert.strictEqual(bridged[0].msgData.content, '回调消息测试'); assert.strictEqual(readProcessedEventIds().has(messageId), true); const roomId = 'room-callback-test'; const groupMessageId = `callback-group-${Date.now()}`; writeConfirmedMapping({ [roomId]: { roomId, roomName: '回调测试客户群', status: 'ACTIVE', reviewStatus: 'CONFIRMED', }, }); const groupEnvelope = { code: 0, source: 'relay', data: [{ guid: 'device-1', cmd: 15000, msgType: 2, msgUniqueIdentifier: groupMessageId, fromRoomId: roomId, senderId: 'customer-2', senderName: '群客户', timestamp: Math.floor(Date.now() / 1000), seq: 2, msgData: { content: '客户群回调新消息' }, }], }; const groupResult = await processWebhookEvents(groupEnvelope); assert.strictEqual(groupResult.processed, 1); assert.strictEqual(groupResult.errors, 0); assert.strictEqual(readRoomMessages(roomId).length, 1); assert.strictEqual(readRoomMessages(roomId)[0].content, '客户群回调新消息'); assert.strictEqual(bridged.length, 1, '群消息只入库和沉淀画像,不得进入客服 Agent'); const duplicateResult = await processWebhookEvents(groupEnvelope); assert.strictEqual(duplicateResult.ignored, 1); assert.strictEqual(readRoomMessages(roomId).length, 1, '重复群回调不得重复入库'); assert.strictEqual(bridged.length, 1, '重复群回调不得进入 Agent'); const unknownRoomId = 'room-callback-unknown'; const unknownResult = await processWebhookEvents({ code: 0, source: 'relay', data: [{ guid: 'device-1', cmd: 15000, msgType: 2, msgUniqueIdentifier: `callback-unknown-${Date.now()}`, fromRoomId: unknownRoomId, senderId: 'customer-3', senderName: '未知群客户', timestamp: Math.floor(Date.now() / 1000), seq: 3, msgData: { content: '未知群消息' }, }], }); assert.strictEqual(unknownResult.ignored, 1); assert.strictEqual(readRoomMessages(unknownRoomId).length, 0, '未确认群不得写入消息库'); assert.strictEqual(bridged.length, 1, '未确认群不得进入 Agent'); const originalFetch = global.fetch; let ackRequests = 0; global.fetch = async url => { if (String(url).endsWith('/api/relay/poll')) { return new Response(JSON.stringify({ success: true, events: [ { eventId: 'broken-event', encryptedPayload: 'not-valid-base64' }, ] }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } if (String(url).endsWith('/api/relay/ack')) { ackRequests += 1; return new Response(JSON.stringify({ success: true, ackedCount: 1 }), { status: 200 }); } throw new Error(`unexpected URL: ${url}`); }; try { const poll = await runPollOnce('https://relay.example.test', 'secret', 'device-1', 'invalid-private-key'); assert.strictEqual(poll.failed, 1); assert.strictEqual(poll.acked, 0); assert.strictEqual(ackRequests, 0); } finally { global.fetch = originalFetch; } console.log('[ok] private callbacks bridge to Agent; group callbacks only archive and deduplicate; unknown groups stay isolated; failed Relay events remain unacked'); } main().finally(() => { delete require.cache[agentServicePath]; try { getGroupOperationsStore().close(); } catch {} fs.rmSync(root, { recursive: true, force: true }); }).catch(error => { console.error(error); process.exitCode = 1; });