callback-relay-smoke-test.js 5.1 KB

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