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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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: 'auto' },
  32. memory: { enabled: false },
  33. agent: { apiKey: 'smoke', model: 'stub', provider: 'stub' },
  34. qiwei: { accountKey, allowedSenders: [], manualAllowedSenders: [], autoEnrolledSenders: [], intakeAutopilotConversationMode: 'auto' },
  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 { content: '普通 Agent 回复草稿', confidence: 0.5, intent: 'general', reason: 'smoke', requiresHuman: true, profileUpdates: {}, citations: [], toolTrace: [] };
  47. },
  48. };
  49. const service = new AgentWorkbenchService({ db, agent, qiwei, config });
  50. const poller = { status: () => ({ running: false }), stop() {} };
  51. return {
  52. dir, db, config, service, sent,
  53. target: { config: config.qiwei, db, service, qiwei },
  54. workbenchTarget: { config, db, service, poller },
  55. setSendSuccess(value) { currentSendSuccess = value; },
  56. close() {
  57. service.close?.();
  58. db.close();
  59. fs.rmSync(dir, { recursive: true, force: true });
  60. },
  61. };
  62. }
  63. function inbound(contactId, overrides = {}) {
  64. return {
  65. msgType: 0,
  66. senderId: contactId,
  67. senderName: `联系人-${contactId}`,
  68. content: '你好,我想咨询一下',
  69. timestamp: Math.floor(Date.now() / 1000),
  70. seq: Math.floor(Math.random() * 1000000),
  71. msgServerId: `message-${contactId}-${Date.now()}-${Math.random()}`,
  72. ...overrides,
  73. };
  74. }
  75. async function main() {
  76. await check('strict mode and group policy create no onboarding records', async () => {
  77. const ctx = createContext();
  78. try {
  79. const strict = await __testing.ingestMessageForWorkbench(ctx.target, inbound('strict-contact'), 'smoke');
  80. assert.equal(strict.status, 'ignored_not_allowlisted');
  81. assert.equal(ctx.db.listConversations().length, 0);
  82. assert.equal(ctx.db.listOnboardings('account-a').length, 0);
  83. const group = evaluatePolledMessage(inbound('group-member', { fromRoomId: 'room-1', chatType: 'group' }), { intakeMode: 'auto_enroll_review', allowedSenders: [] });
  84. assert.equal(group.reason, 'group_message');
  85. } finally { ctx.close(); }
  86. });
  87. await check('review auto enrollment creates one welcome draft with zero sends', async () => {
  88. const ctx = createContext({ mode: 'auto_enroll_review', welcomeEnabled: true });
  89. try {
  90. const first = await __testing.ingestMessageForWorkbench(ctx.target, inbound('review-contact'), 'smoke');
  91. assert.equal(first.status, 'pending_review');
  92. assert.equal(ctx.db.getConversationByContactId('review-contact').mode, 'review');
  93. assert.equal(ctx.sent.length, 0);
  94. const onboarding = ctx.db.getOnboarding('account-a', 'review-contact');
  95. assert.ok(onboarding.first_message_id);
  96. assert.ok(onboarding.welcome_draft_id);
  97. const restarted = { ...ctx.config.qiwei, allowedSenders: ['review-contact'], autoEnrolledSenders: ['review-contact'] };
  98. await __testing.ingestMessageForWorkbench({ ...ctx.target, config: restarted }, inbound('review-contact', { content: '继续了解办理流程' }), 'restart');
  99. assert.equal(ctx.db.listDrafts({ conversationId: onboarding.conversation_id }).filter(item => item.intent === 'new_contact_welcome').length, 1);
  100. assert.equal(ctx.sent.length, 0);
  101. } finally { ctx.close(); }
  102. });
  103. await check('generic automatic intake requires confirmation and maps to auto mode', async () => {
  104. const ctx = createContext();
  105. try {
  106. assert.throws(() => __testing.updateIntakePolicyForWorkbench(ctx, { mode: 'auto_enroll_autopilot', welcomeEnabled: true, welcomeText: '欢迎', welcomeSendMode: 'send' }), /二次确认/);
  107. const policy = __testing.updateIntakePolicyForWorkbench(ctx, { mode: 'auto_enroll_autopilot', welcomeEnabled: true, welcomeText: '欢迎', welcomeSendMode: 'send', confirmation: 'ENABLE_AUTO_ENROLL_AUTOPILOT' }, 'smoke-admin');
  108. assert.equal(policy.autopilotConfirmed, true);
  109. assert.equal(policy.effectiveConversationMode, 'auto');
  110. } finally { ctx.close(); }
  111. });
  112. await check('failed welcome can be retried once and then stays sent', async () => {
  113. const ctx = createContext({ mode: 'auto_enroll_autopilot', welcomeEnabled: true, welcomeSendMode: 'send', sendSuccess: false });
  114. try {
  115. const first = await __testing.ingestMessageForWorkbench(ctx.target, inbound('send-contact'), 'smoke');
  116. assert.equal(first.status, 'welcome_send_failed');
  117. assert.equal(ctx.db.getConversationByContactId('send-contact').mode, 'auto');
  118. assert.equal(ctx.db.getOnboarding('account-a', 'send-contact').attempt_count, 1);
  119. ctx.setSendSuccess(true);
  120. assert.equal((await ctx.service.retryOnboardingWelcome('send-contact', 'smoke-admin')).status, 'welcome_sent');
  121. assert.equal(ctx.db.getOnboarding('account-a', 'send-contact').attempt_count, 2);
  122. await assert.rejects(() => ctx.service.retryOnboardingWelcome('send-contact', 'smoke-admin'), /明确发送失败/);
  123. } finally { ctx.close(); }
  124. });
  125. await check('two accounts keep manual, automatic and listener state isolated', async () => {
  126. const accountA = createContext({ accountKey: 'account-a' });
  127. const accountB = createContext({ accountKey: 'account-b', mode: 'auto_enroll_review' });
  128. try {
  129. __testing.updateAllowlistForWorkbench(accountA.workbenchTarget, { contactIds: ['contact-a'], autoStart: true });
  130. assert.deepEqual(accountA.config.qiwei.allowedSenders, ['contact-a']);
  131. assert.deepEqual(accountB.config.qiwei.allowedSenders, []);
  132. assert.equal(accountA.db.getSetting('listener_enabled', 'false'), 'true');
  133. assert.equal(accountB.db.getSetting('listener_enabled', 'false'), 'false');
  134. await __testing.ingestMessageForWorkbench(accountB.target, inbound('contact-b'), 'smoke-b');
  135. __testing.hydrateAccountAllowlist(accountA.config, accountA.db);
  136. __testing.hydrateAccountAllowlist(accountB.config, accountB.db);
  137. assert.deepEqual(accountA.config.qiwei.allowedSenders, ['contact-a']);
  138. assert.deepEqual(accountB.config.qiwei.allowedSenders, ['contact-b']);
  139. } finally { accountA.close(); accountB.close(); }
  140. });
  141. console.log(JSON.stringify({ status: 'ok', passed: results.length, results }, null, 2));
  142. }
  143. main()
  144. .finally(() => fs.rmSync(messagesDir, { recursive: true, force: true }))
  145. .catch(error => { console.error(error); process.exitCode = 1; });