agent-intake-policy-smoke-test.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. const assert = require('assert/strict');
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const messagesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-generic-intake-messages-'));
  6. process.env.QIWEI_MESSAGES_DIR = messagesDir;
  7. const { AgentWorkbenchDb } = require('../mcp/src/core/agent-workbench-db');
  8. const { AgentWorkbenchService } = require('../mcp/src/core/agent-workbench-service');
  9. const { evaluatePolledMessage } = require('../mcp/src/core/agent-poller-policy');
  10. const { __testing } = require('../mcp/src/dashboard/agent-service');
  11. const results = [];
  12. async function check(name, fn) { await fn(); results.push({ name, status: 'passed' }); }
  13. function createContext({ accountKey = 'account-a', mode = 'allowlist_only', welcomeEnabled = false, welcomeSendMode = 'draft', sendSuccess = true } = {}) {
  14. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-generic-intake-'));
  15. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), {
  16. globalPaused: false,
  17. defaultMode: 'review',
  18. autoSendConfidence: 0.88,
  19. personalIntakeMode: mode,
  20. welcomeEnabled,
  21. welcomeText: '您好,已经收到您的消息。',
  22. welcomeSendMode,
  23. });
  24. if (mode === 'auto_enroll_autopilot') {
  25. db.setIntakePolicy({ mode, welcomeEnabled, welcomeText: '您好,已经收到您的消息。', welcomeSendMode, autopilotConfirmation: 'v1' });
  26. }
  27. let currentSendSuccess = sendSuccess;
  28. const sent = [];
  29. const config = {
  30. accountKey,
  31. intake: { effectiveAutopilotMode: 'autopilot' },
  32. memory: { enabled: false },
  33. agent: { apiKey: 'smoke', model: 'stub', provider: 'stub' },
  34. qiwei: { accountKey, allowedSenders: [], manualAllowedSenders: [], autoEnrolledSenders: [], intakeAutopilotConversationMode: 'autopilot' },
  35. };
  36. const qiwei = {
  37. isConfigured: () => true,
  38. async sendText(contactId, content) {
  39. sent.push({ contactId, content });
  40. return { isSendSuccess: currentSendSuccess };
  41. },
  42. };
  43. const agent = {
  44. modelClient: { isConfigured: () => true },
  45. async run() {
  46. return {
  47. content: '可以按您的节奏继续;您更关心材料清单还是办理时间?',
  48. confidence: 0.95,
  49. intent: 'general',
  50. reason: 'smoke',
  51. requiresHuman: false,
  52. profileUpdates: {},
  53. citations: [],
  54. toolTrace: [],
  55. };
  56. },
  57. };
  58. const service = new AgentWorkbenchService({ db, agent, qiwei, config });
  59. const poller = { status: () => ({ running: false }), stop() {} };
  60. return {
  61. dir, db, config, service, sent,
  62. target: { config: config.qiwei, db, service, qiwei },
  63. workbenchTarget: { config, db, service, poller },
  64. setSendSuccess(value) { currentSendSuccess = value; },
  65. close() {
  66. service.close?.();
  67. db.close();
  68. fs.rmSync(dir, { recursive: true, force: true });
  69. },
  70. };
  71. }
  72. function inbound(contactId, overrides = {}) {
  73. return {
  74. msgType: 0,
  75. senderId: contactId,
  76. senderName: `联系人-${contactId}`,
  77. content: '你好,我想咨询一下',
  78. timestamp: Math.floor(Date.now() / 1000),
  79. seq: Math.floor(Math.random() * 1000000),
  80. msgServerId: `message-${contactId}-${Date.now()}-${Math.random()}`,
  81. ...overrides,
  82. };
  83. }
  84. async function main() {
  85. await check('strict mode and group policy create no onboarding records', async () => {
  86. const ctx = createContext();
  87. try {
  88. const strict = await __testing.ingestMessageForWorkbench(ctx.target, inbound('strict-contact'), 'smoke');
  89. assert.equal(strict.status, 'ignored_not_allowlisted');
  90. assert.equal(ctx.db.listConversations().length, 0);
  91. assert.equal(ctx.db.listOnboardings('account-a').length, 0);
  92. const group = evaluatePolledMessage(inbound('group-member', { fromRoomId: 'room-1', chatType: 'group' }), { intakeMode: 'auto_enroll_review', allowedSenders: [] });
  93. assert.equal(group.reason, 'group_message');
  94. } finally { ctx.close(); }
  95. });
  96. await check('review auto enrollment creates one welcome draft with zero sends', async () => {
  97. const ctx = createContext({ mode: 'auto_enroll_review', welcomeEnabled: true });
  98. try {
  99. const first = await __testing.ingestMessageForWorkbench(ctx.target, inbound('review-contact'), 'smoke');
  100. assert.equal(first.status, 'pending_review');
  101. assert.equal(ctx.db.getConversationByContactId('review-contact').mode, 'review');
  102. assert.equal(ctx.sent.length, 0);
  103. const onboarding = ctx.db.getOnboarding('account-a', 'review-contact');
  104. assert.ok(onboarding.first_message_id);
  105. assert.ok(onboarding.welcome_draft_id);
  106. const restarted = { ...ctx.config.qiwei, allowedSenders: ['review-contact'], autoEnrolledSenders: ['review-contact'] };
  107. await __testing.ingestMessageForWorkbench({ ...ctx.target, config: restarted }, inbound('review-contact', { content: '继续了解办理流程' }), 'restart');
  108. assert.equal(ctx.db.listDrafts({ conversationId: onboarding.conversation_id }).filter(item => item.intent === 'new_contact_welcome').length, 1);
  109. assert.equal(ctx.sent.length, 0);
  110. } finally { ctx.close(); }
  111. });
  112. await check('generic automatic intake requires confirmation and maps to full autopilot mode', async () => {
  113. const ctx = createContext();
  114. try {
  115. assert.throws(() => __testing.updateIntakePolicyForWorkbench(ctx, { mode: 'auto_enroll_autopilot', welcomeEnabled: true, welcomeText: '欢迎', welcomeSendMode: 'send' }), /二次确认/);
  116. const policy = __testing.updateIntakePolicyForWorkbench(ctx, { mode: 'auto_enroll_autopilot', welcomeEnabled: true, welcomeText: '欢迎', welcomeSendMode: 'send', confirmation: 'ENABLE_AUTO_ENROLL_AUTOPILOT' }, 'smoke-admin');
  117. assert.equal(policy.autopilotConfirmed, true);
  118. assert.equal(policy.effectiveConversationMode, 'autopilot');
  119. } finally { ctx.close(); }
  120. });
  121. await check('failed welcome can be retried once and then stays sent', async () => {
  122. const ctx = createContext({ mode: 'auto_enroll_autopilot', welcomeEnabled: true, welcomeSendMode: 'send', sendSuccess: false });
  123. try {
  124. const first = await __testing.ingestMessageForWorkbench(ctx.target, inbound('send-contact'), 'smoke');
  125. assert.equal(first.status, 'welcome_send_failed');
  126. assert.equal(ctx.db.getConversationByContactId('send-contact').mode, 'autopilot');
  127. assert.equal(ctx.db.getOnboarding('account-a', 'send-contact').attempt_count, 1);
  128. ctx.setSendSuccess(true);
  129. assert.equal((await ctx.service.retryOnboardingWelcome('send-contact', 'smoke-admin')).status, 'welcome_sent');
  130. assert.equal(ctx.db.getOnboarding('account-a', 'send-contact').attempt_count, 2);
  131. await assert.rejects(() => ctx.service.retryOnboardingWelcome('send-contact', 'smoke-admin'), /明确发送失败/);
  132. const next = await __testing.ingestMessageForWorkbench(ctx.target, inbound('send-contact', { content: '继续咨询' }), 'smoke-next');
  133. assert.equal(next.status, 'autopilot_sent');
  134. assert.equal(ctx.db.listDrafts({ conversationId: next.conversation.id }).length, 0);
  135. } finally { ctx.close(); }
  136. });
  137. await check('two accounts keep manual, automatic and listener state isolated', async () => {
  138. const accountA = createContext({ accountKey: 'account-a' });
  139. const accountB = createContext({ accountKey: 'account-b', mode: 'auto_enroll_review' });
  140. try {
  141. __testing.updateAllowlistForWorkbench(accountA.workbenchTarget, { contactIds: ['contact-a'], autoStart: true });
  142. assert.deepEqual(accountA.config.qiwei.allowedSenders, ['contact-a']);
  143. assert.deepEqual(accountB.config.qiwei.allowedSenders, []);
  144. assert.equal(accountA.db.getSetting('listener_enabled', 'false'), 'true');
  145. assert.equal(accountB.db.getSetting('listener_enabled', 'false'), 'false');
  146. await __testing.ingestMessageForWorkbench(accountB.target, inbound('contact-b'), 'smoke-b');
  147. __testing.hydrateAccountAllowlist(accountA.config, accountA.db);
  148. __testing.hydrateAccountAllowlist(accountB.config, accountB.db);
  149. assert.deepEqual(accountA.config.qiwei.allowedSenders, ['contact-a']);
  150. assert.deepEqual(accountB.config.qiwei.allowedSenders, ['contact-b']);
  151. } finally { accountA.close(); accountB.close(); }
  152. });
  153. console.log(JSON.stringify({ status: 'ok', passed: results.length, results }, null, 2));
  154. }
  155. main()
  156. .finally(() => fs.rmSync(messagesDir, { recursive: true, force: true }))
  157. .catch(error => { console.error(error); process.exitCode = 1; });