group-operations-smoke-test.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #!/usr/bin/env node
  2. const assert = require('assert');
  3. const fs = require('fs');
  4. const os = require('os');
  5. const path = require('path');
  6. const { GroupOperationsStore } = require('../mcp/src/core/group-operations-store');
  7. const { __testing: groupOpsTesting } = require('../mcp/src/tools/qiwei-group-operations-run');
  8. const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-group-ops-'));
  9. const store = new GroupOperationsStore(path.join(tempDir, 'test.db'));
  10. try {
  11. const account = 'account-a';
  12. const otherAccount = 'account-b';
  13. const directoryAccount = 'directory-account';
  14. assert.equal(groupOpsTesting.syncCustomerGroupsToOperations(store, directoryAccount, [
  15. { roomId: 'confirmed-room', roomName: 'Confirmed room', reviewStatus: 'CONFIRMED' },
  16. { roomId: 'auto-room', roomName: 'Auto room', reviewStatus: 'AUTO_CONFIRMED' },
  17. { roomId: 'suggested-room', roomName: 'Suggested room', reviewStatus: 'SUGGESTED' },
  18. ]), 2);
  19. assert.deepEqual(store.listGroups(directoryAccount).map(group => group.room_id).sort(), ['auto-room', 'confirmed-room']);
  20. assert.equal(groupOpsTesting.syncCustomerGroupsToOperations(store, directoryAccount, [
  21. { roomId: 'confirmed-room', roomName: 'Confirmed room', reviewStatus: 'CONFIRMED' },
  22. { roomId: 'auto-room', roomName: 'Auto room', reviewStatus: 'AUTO_CONFIRMED' },
  23. ]), 0, 'directory reconciliation must be idempotent');
  24. const playbook = store.createPlaybook(account, {
  25. name: '新客群服务 SOP',
  26. groupType: 'new_customer',
  27. content: { cadence: [{ time: '09:30', type: 'welcome', objective: '欢迎新客户', template: '欢迎加入 {{room_name}}' }] }
  28. }, 'test');
  29. assert.equal(playbook.status, 'draft');
  30. const published = store.publishPlaybook(account, playbook.id, 1, 'test');
  31. assert.equal(published.status, 'published');
  32. store.upsertGroup(account, { roomId: 'room-1', roomName: '客户服务群', storeId: '门店A', groupType: 'new_customer', playbookId: playbook.id }, 'test');
  33. store.upsertGroup(account, { roomId: 'room-2', roomName: '会员服务群', storeId: '门店B', groupType: 'new_customer', playbookId: playbook.id }, 'test');
  34. assert.equal(store.listGroups(account).length, 2);
  35. assert.equal(store.listGroups(otherAccount).length, 0, '账号数据必须隔离');
  36. const version2 = store.addPlaybookVersion(account, playbook.id, { cadence: [{ time: '10:00', type: 'welcome', objective: '新版欢迎', template: '新版欢迎 {{room_name}}' }] }, '升级欢迎话术', 'test');
  37. assert.equal(version2.version, 2);
  38. const stillPublished = store.getPlaybook(account, playbook.id);
  39. assert.equal(stillPublished.status, 'published', '创建草稿版本不能停用当前已发布 SOP');
  40. assert.equal(stillPublished.version, 1, '创建草稿版本后默认读取仍应是当前已发布版本');
  41. const planBeforeV2Publish = store.generatePlan(account, { roomId: 'room-1', planDate: '2026-07-17' }, 'test');
  42. assert.equal(planBeforeV2Publish.items[0].draft_content, '欢迎加入 客户服务群', '未发布草稿不能进入运营计划');
  43. assert.equal(planBeforeV2Publish.playbook_version_id, stillPublished.current_version_id, '计划审计版本必须与实际内容一致');
  44. const impact = store.previewPlaybookPublish(account, playbook.id, 2);
  45. assert.equal(impact.affectedGroupCount, 2);
  46. assert.equal(impact.changes.length > 0, true);
  47. store.publishPlaybook(account, playbook.id, 2, 'test');
  48. store.publishPlaybook(account, playbook.id, 1, 'test');
  49. const plan = store.generatePlan(account, { roomId: 'room-1', planDate: '2026-07-18' }, 'test');
  50. assert.equal(plan.items.length, 1);
  51. assert.match(plan.items[0].draft_content, /客户服务群/);
  52. const duplicate = store.generatePlan(account, { roomId: 'room-1', planDate: '2026-07-18' }, 'test');
  53. assert.equal(duplicate.id, plan.id, '同群同日计划必须幂等');
  54. const approved = store.updatePlanItem(account, plan.items[0].id, 'approve', {}, 'test');
  55. assert.equal(approved.status, 'approved');
  56. store.updatePlanItem(account, plan.items[0].id, 'mark_sent', { externalMessageId: 'manual-1' }, 'test');
  57. const batch = store.batchGeneratePlans(account, { planDate: '2026-07-19', storeId: '门店B' }, 'test');
  58. assert.equal(batch.total, 1);
  59. assert.equal(batch.success, 1);
  60. const automationPlan = store.getPlan(batch.results[0].planId);
  61. const automationItem = store.updatePlanItem(account, automationPlan.items[0].id, 'approve', {}, 'test');
  62. const policy = store.createAutomationPolicy(account, { policy: { enabled: true, mode: 'dry_run', allowedGroupIds: ['room-2'], allowedTypes: [automationItem.type], windows: [{ days: [1], start: '09:00', end: '18:00' }], dailyLimitPerGroup: 1, weeklyLimitPerGroup: 7, consecutiveFailureLimit: 3, grayPercent: 100, requireNoOpenHighRisk: true } }, 'test');
  63. assert.equal(policy.status, 'draft');
  64. store.activateAutomationPolicy(account, policy.id, 'test');
  65. const automationAt = new Date('2026-07-20T10:00:00+08:00');
  66. assert.equal(store.evaluateAutomation(account, store.getPlanItemWithContext(account, automationItem.id), automationAt).allowed, false, '激活后必须保持暂停');
  67. store.updateAutomationState(account, 'resume', 'test');
  68. const decision = store.evaluateAutomation(account, store.getPlanItemWithContext(account, automationItem.id), automationAt);
  69. assert.equal(decision.allowed, true);
  70. assert.equal(store.createSendAttempt(account, store.getPlanItemWithContext(account, automationItem.id), decision, 'dry_run').status, 'dry_run');
  71. for (let index = 0; index < 3; index++) {
  72. const attempt = store.createSendAttempt(account, store.getPlanItemWithContext(account, automationItem.id), decision, 'live');
  73. store.finishSendAttempt(account, attempt.id, { success: false, error: `模拟失败 ${index + 1}` });
  74. }
  75. assert.equal(store.getAutomationState(account).breaker_open, 1, '连续失败必须熔断');
  76. assert.equal(store.evaluateAutomation(account, store.getPlanItemWithContext(account, automationItem.id), automationAt).reasons.includes('熔断器已打开'), true);
  77. store.updateAutomationState(account, 'reset_breaker', 'test');
  78. assert.equal(store.getAutomationState(account).breaker_open, 0);
  79. const ambiguousAttempt = store.createSendAttempt(account, store.getPlanItemWithContext(account, automationItem.id), decision, 'live');
  80. assert.equal(store.finishSendAttempt(account, ambiguousAttempt.id, { success: false, ambiguous: true, error: '发送回执超时' }).status, 'unknown');
  81. assert.equal(store.evaluateAutomation(account, store.getPlanItemWithContext(account, automationItem.id), automationAt).reasons.some(reason => reason.includes('结果待确认')), true, '回执未知时必须锁住重试');
  82. store.updateAutomationState(account, 'pause', 'test');
  83. const review = store.reviewMessages(account, {
  84. roomId: 'room-1', slaMinutes: 30, messages: [
  85. { messageId: 'm1', senderId: 'customer-1', senderRole: 'customer', content: '这个问题怎么退款?', sentAt: '2026-07-18T08:00:00.000Z' }
  86. ]
  87. }, 'test');
  88. assert.equal(review.findingCount, 2, '应识别退款风险和超时未回应');
  89. const unrelated = store.reviewMessages(account, {
  90. roomId: 'room-2', slaMinutes: 30, messages: [
  91. { messageId: 'q-unrelated', senderId: 'customer-2', senderRole: 'customer', content: '退款什么时候到账?', sentAt: '2026-07-18T08:00:00.000Z' },
  92. { messageId: 'a-unrelated', senderId: 'staff-2', senderRole: 'staff', content: '今天天气不错', sentAt: '2026-07-18T08:01:00.000Z' }
  93. ]
  94. }, 'test');
  95. assert.equal(unrelated.findings.some(item => item.type === 'unanswered_question'), true, '无关员工消息不能算有效回应');
  96. const answered = store.reviewMessages(account, {
  97. roomId: 'room-2', slaMinutes: 30, messages: [
  98. { messageId: 'a-related', senderId: 'staff-2', senderRole: 'staff', content: '退款已提交,预计今天下午原路到账。', sentAt: '2026-07-18T08:02:00.000Z' }
  99. ]
  100. }, 'test');
  101. assert.equal(answered.findings.filter(item => item.type === 'unanswered_question').length, 0, '有效回复后本次质检不应新增未回应结论');
  102. const configured = store.setQualitySettings(account, { slaMinutes: 45, riskTerms: ['投诉', '退款'], forbiddenClaims: ['保证'] }, 'test');
  103. assert.equal(configured.sla_minutes, 45);
  104. const overview = store.overview(account, '2026-07-18');
  105. assert.equal(overview.metrics.executionRate, 100);
  106. assert.equal(overview.metrics.highRisk >= 1, true);
  107. assert.equal(Array.isArray(overview.scores), true);
  108. assert.equal(overview.scores.length, 2);
  109. assert.equal(overview.scores.every(item => item.coverage >= 0 && item.coverage <= 100), true);
  110. store.updateFinding(account, overview.findings[0].id, { status: 'false_positive', resolution: '测试误报' }, 'test');
  111. assert.equal(store.feedbackSummary(account).falsePositives, 1);
  112. const task = store.createTask(account, { roomId: 'room-1', findingId: overview.findings[1].id, title: '跟进客户退款问题', owner: '测试负责人', dueAt: '2026-07-20T18:00:00+08:00', priority: 'high' }, 'test');
  113. assert.equal(task.status, 'open');
  114. assert.equal(store.updateTask(account, task.id, { status: 'done' }, 'test').status, 'done');
  115. assert.equal(store.storeBreakdown(account, '2026-07-18').length, 2);
  116. store.reviewMessages(account, {
  117. roomId: 'room-1',
  118. messages: [
  119. { messageId: 'recent-activity', senderId: 'customer-1', senderRole: 'customer', content: '收到,谢谢', sentAt: new Date().toISOString() }
  120. ]
  121. }, 'test');
  122. assert.equal(store.groupInsights(account, 'room-1')[0].activity.currentMessages >= 1, true);
  123. assert.equal(store.excellentScripts(account).length, 1);
  124. assert.equal(store.overview(otherAccount, '2026-07-18').metrics.groups, 0);
  125. console.log('group-operations smoke: ok');
  126. } finally {
  127. store.close();
  128. fs.rmSync(tempDir, { recursive: true, force: true });
  129. }