callback-relay-smoke-test.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. 'use strict';
  2. const assert = require('assert');
  3. const fs = require('fs');
  4. const os = require('os');
  5. const path = require('path');
  6. const root = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-callback-relay-smoke-'));
  7. process.env.QIWEI_OUTPUTS_DIR = root;
  8. const agentServicePath = require.resolve('../mcp/src/dashboard/agent-service');
  9. const bridged = [];
  10. require.cache[agentServicePath] = {
  11. id: agentServicePath,
  12. filename: agentServicePath,
  13. loaded: true,
  14. exports: {
  15. ingestWebhookMessage: async message => {
  16. bridged.push(message);
  17. return { status: 'pending_review' };
  18. },
  19. },
  20. };
  21. const { processWebhookEvents } = require('../mcp/src/core/webhook-processor');
  22. const { readProcessedEventIds } = require('../mcp/src/core/webhook-store');
  23. const { getGroupOperationsStore } = require('../mcp/src/core/group-operations-store');
  24. const {
  25. writeConfirmedMapping,
  26. readRoomMessages,
  27. } = require('../mcp/src/tools/qiwei-group-management-run');
  28. const { runPollOnce } = require('./start-relay-client');
  29. async function main() {
  30. const messageId = `callback-private-${Date.now()}`;
  31. const result = await processWebhookEvents({
  32. code: 0,
  33. msg: 'test',
  34. source: 'relay',
  35. data: [{
  36. guid: 'device-1', cmd: 15000, msgType: 1, msgUniqueIdentifier: messageId,
  37. senderId: 'customer-1', senderName: '测试客户', receiverId: 'staff-1',
  38. timestamp: Math.floor(Date.now() / 1000), seq: 1,
  39. msgData: { content: '回调消息测试' },
  40. }],
  41. });
  42. assert.strictEqual(result.processed, 1);
  43. assert.strictEqual(result.errors, 0);
  44. assert.strictEqual(bridged.length, 1);
  45. assert.strictEqual(bridged[0].msgData.content, '回调消息测试');
  46. assert.strictEqual(readProcessedEventIds().has(messageId), true);
  47. const roomId = 'room-callback-test';
  48. const groupMessageId = `callback-group-${Date.now()}`;
  49. writeConfirmedMapping({
  50. [roomId]: {
  51. roomId,
  52. roomName: '回调测试客户群',
  53. status: 'ACTIVE',
  54. reviewStatus: 'CONFIRMED',
  55. },
  56. });
  57. const groupEnvelope = {
  58. code: 0,
  59. source: 'relay',
  60. data: [{
  61. guid: 'device-1', cmd: 15000, msgType: 2, msgUniqueIdentifier: groupMessageId,
  62. fromRoomId: roomId, senderId: 'customer-2', senderName: '群客户',
  63. timestamp: Math.floor(Date.now() / 1000), seq: 2,
  64. msgData: { content: '客户群回调新消息' },
  65. }],
  66. };
  67. const groupResult = await processWebhookEvents(groupEnvelope);
  68. assert.strictEqual(groupResult.processed, 1);
  69. assert.strictEqual(groupResult.errors, 0);
  70. assert.strictEqual(readRoomMessages(roomId).length, 1);
  71. assert.strictEqual(readRoomMessages(roomId)[0].content, '客户群回调新消息');
  72. assert.strictEqual(bridged.length, 1, '群消息只入库和沉淀画像,不得进入客服 Agent');
  73. const duplicateResult = await processWebhookEvents(groupEnvelope);
  74. assert.strictEqual(duplicateResult.ignored, 1);
  75. assert.strictEqual(readRoomMessages(roomId).length, 1, '重复群回调不得重复入库');
  76. assert.strictEqual(bridged.length, 1, '重复群回调不得进入 Agent');
  77. const unknownRoomId = 'room-callback-unknown';
  78. const unknownResult = await processWebhookEvents({
  79. code: 0,
  80. source: 'relay',
  81. data: [{
  82. guid: 'device-1', cmd: 15000, msgType: 2,
  83. msgUniqueIdentifier: `callback-unknown-${Date.now()}`,
  84. fromRoomId: unknownRoomId, senderId: 'customer-3', senderName: '未知群客户',
  85. timestamp: Math.floor(Date.now() / 1000), seq: 3,
  86. msgData: { content: '未知群消息' },
  87. }],
  88. });
  89. assert.strictEqual(unknownResult.ignored, 1);
  90. assert.strictEqual(readRoomMessages(unknownRoomId).length, 0, '未确认群不得写入消息库');
  91. assert.strictEqual(bridged.length, 1, '未确认群不得进入 Agent');
  92. const originalFetch = global.fetch;
  93. let ackRequests = 0;
  94. global.fetch = async url => {
  95. if (String(url).endsWith('/api/relay/poll')) {
  96. return new Response(JSON.stringify({ success: true, events: [
  97. { eventId: 'broken-event', encryptedPayload: 'not-valid-base64' },
  98. ] }), { status: 200, headers: { 'Content-Type': 'application/json' } });
  99. }
  100. if (String(url).endsWith('/api/relay/ack')) {
  101. ackRequests += 1;
  102. return new Response(JSON.stringify({ success: true, ackedCount: 1 }), { status: 200 });
  103. }
  104. throw new Error(`unexpected URL: ${url}`);
  105. };
  106. try {
  107. const poll = await runPollOnce('https://relay.example.test', 'secret', 'device-1', 'invalid-private-key');
  108. assert.strictEqual(poll.failed, 1);
  109. assert.strictEqual(poll.acked, 0);
  110. assert.strictEqual(ackRequests, 0);
  111. } finally {
  112. global.fetch = originalFetch;
  113. }
  114. console.log('[ok] private callbacks bridge to Agent; group callbacks only archive and deduplicate; unknown groups stay isolated; failed Relay events remain unacked');
  115. }
  116. main().finally(() => {
  117. delete require.cache[agentServicePath];
  118. try { getGroupOperationsStore().close(); } catch {}
  119. fs.rmSync(root, { recursive: true, force: true });
  120. }).catch(error => {
  121. console.error(error);
  122. process.exitCode = 1;
  123. });