message-types-smoke-test.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. #!/usr/bin/env node
  2. 'use strict';
  3. const assert = require('assert');
  4. const fs = require('fs');
  5. const path = require('path');
  6. const { normalizeMessage, detectMessageType } = require('../mcp/src/core/message-normalizer');
  7. const { sanitizePayload } = require('../mcp/src/providers/fmode-wecom-gateway');
  8. const samples = [
  9. ['text', 2, { content: 'hello' }, 'hello'],
  10. ['image', 101, { filename: 'sample.png' }, '[图片] sample.png'],
  11. ['voice', 16, { duration: 3 }, '[语音] 3 秒'],
  12. ['video', 103, { filename: 'sample.mp4' }, '[视频] sample.mp4'],
  13. ['file', 20, { filename: 'document.pdf' }, '[文件] document.pdf'],
  14. ['weapp', 78, { title: '服务小程序' }, '[小程序] 服务小程序'],
  15. ['meeting', 0, { title: '项目例会' }, '[会议邀请] 项目例会'],
  16. ['location', 6, { title: '会议室', latitude: 1, longitude: 2 }, '[位置] 会议室'],
  17. ['link', 13, { title: '帮助中心', url: 'https://example.invalid/help' }, '[链接] 帮助中心 · https://example.invalid/help'],
  18. ['card', 41, { displayName: '测试联系人' }, '[名片] 测试联系人'],
  19. ['emotion', 104, { title: '微笑' }, '[表情] 微笑'],
  20. ['revoke', 2063, { title: '消息已撤回' }, '[撤回消息] 消息已撤回'],
  21. ['agree', 0, { title: '同意存档' }, '[同意会话存档] 同意存档'],
  22. ['disagree', 0, { title: '不同意存档' }, '[不同意会话存档] 不同意存档'],
  23. ['chatrecord', 4, { title: '会话记录', itemCount: 2 }, '[会话记录] 会话记录 · 2 条'],
  24. ['todo', 0, { title: '跟进事项' }, '[待办] 跟进事项'],
  25. ['vote', 0, { title: '方案投票' }, '[投票] 方案投票'],
  26. ['collect', 0, { title: '信息登记' }, '[填表] 信息登记'],
  27. ['redpacket', 26, { title: '红包' }, '[红包] 红包'],
  28. ['external_redpacket', 0, { title: '互通红包' }, '[互通红包] 互通红包'],
  29. ['transfer', 588, { title: '转账' }, '[转账] 转账'],
  30. ['meeting_notification', 0, { title: '会议开始' }, '[会议控制] 会议开始'],
  31. ['docmsg', 0, { title: '在线文档' }, '[在线文档] 在线文档'],
  32. ['markdown', 2, { content: '**Markdown**' }, '**Markdown**'],
  33. ['news', 0, { title: '图文消息' }, '[图文消息] 图文消息'],
  34. ['calendar', 0, { title: '日程' }, '[日程] 日程'],
  35. ['mixed', 0, { title: '混合内容' }, '[混合消息] 混合内容'],
  36. ['meeting_voice_call', 0, { title: '会议音频' }, '[音频存档] 会议音频'],
  37. ['voip_doc_share', 0, { title: '共享文档' }, '[音频共享文档] 共享文档'],
  38. ['sphfeed', 141, { title: '视频号内容' }, '[视频号] 视频号内容'],
  39. ['voiptext', 0, { callDuration: 12 }, '[音视频通话] 12 秒'],
  40. ['qydiskfile', 15, { filename: '微盘文件.pdf' }, '[微盘文件] 微盘文件.pdf'],
  41. ['solitaire', 0, { title: '接龙' }, '[接龙] 接龙'],
  42. ['note', 0, { title: '笔记' }, '[笔记] 笔记'],
  43. ];
  44. for (const [type, msgType, msgData, expected] of samples) {
  45. const normalized = normalizeMessage({ msgType, msgData: { type, ...msgData } });
  46. assert.strictEqual(normalized.type, type, `${type} should retain its declared type`);
  47. assert.strictEqual(normalized.content, expected, `${type} should produce stable display content`);
  48. }
  49. assert.strictEqual(detectMessageType({ msgType: 16, msgData: { fileId: 'voice-file' } }), 'voice');
  50. assert.strictEqual(detectMessageType({ msgType: 103, msgData: { fileId: 'video-file' } }), 'video');
  51. assert.strictEqual(detectMessageType({ msgType: 104, msgData: { fileId: 'emotion-file' } }), 'emotion');
  52. const redacted = sanitizePayload('SERVICE_PASSWORD=example-secret\n"masterKey": "example-key"\nsk-example1234567890');
  53. assert.ok(!redacted.includes('example-secret') && !redacted.includes('example-key') && !redacted.includes('sk-example'), 'public message text should redact credential assignments');
  54. const dashboardRoot = path.join(__dirname, '..', 'mcp', 'src', 'dashboard');
  55. const app = fs.readFileSync(path.join(dashboardRoot, 'app.js'), 'utf8');
  56. const styles = fs.readFileSync(path.join(dashboardRoot, 'styles.css'), 'utf8');
  57. const server = fs.readFileSync(path.join(dashboardRoot, 'server.js'), 'utf8');
  58. for (const contract of [
  59. 'agentCardMessageHtml',
  60. 'agentReplyQuoteHtml',
  61. 'agentFinancialMessageHtml',
  62. 'agentChatrecordItemText',
  63. 'safeAgentMediaUrl',
  64. 'agentMessageData(message).reply',
  65. 'data.cards',
  66. 'subMsgTimestamp',
  67. 'data-agent-chatrecord-open',
  68. 'agent-video-play',
  69. 'openSessionConversation',
  70. 'view-session',
  71. ]) {
  72. assert.ok(app.includes(contract), `dashboard app should include ${contract}`);
  73. }
  74. const richRenderer = app.slice(app.indexOf('function agentCardMessageHtml'), app.indexOf('function renderAgentMessage'));
  75. const messageRenderer = app.slice(app.indexOf('function renderAgentMessage'), app.indexOf('function agentModeLabel'));
  76. for (const type of [
  77. 'image', 'video', 'file', 'weapp', 'meeting', 'location', 'link', 'card', 'emotion', 'chatrecord',
  78. 'todo', 'vote', 'collect', 'redpacket', 'external_redpacket', 'transfer', 'docmsg', 'news', 'calendar',
  79. 'mixed', 'meeting_voice_call', 'voip_doc_share', 'sphfeed', 'voiptext', 'qydiskfile', 'solitaire', 'note',
  80. ]) {
  81. assert.ok(richRenderer.includes(`'${type}'`), `${type} should have a rich-message render path`);
  82. }
  83. for (const type of ['revoke', 'meeting_notification', 'agree', 'disagree']) {
  84. assert.ok(messageRenderer.includes(type), `${type} should have a system-message render path`);
  85. }
  86. assert.ok(messageRenderer.includes("type === 'voice'"), 'voice should have an audio render path');
  87. assert.ok(messageRenderer.includes('agentTextMessageHtml(message)'), 'text and markdown should retain quoted-message rendering');
  88. for (const contract of [
  89. '.agent-message-quote',
  90. '.agent-financial-foot',
  91. '.agent-file-icon',
  92. '.agent-chatrecord-avatar',
  93. '.agent-feed-placeholder',
  94. '.agent-video-play',
  95. ]) {
  96. assert.ok(styles.includes(contract), `dashboard styles should include ${contract}`);
  97. }
  98. for (const route of ['session-transcript', '/media', '/open', 'wechat-emojis']) {
  99. assert.ok(server.includes(route), `dashboard server should include ${route}`);
  100. }
  101. console.log(`message types smoke passed (${samples.length} declared types + 3 numeric precedence checks + public redaction)`);