group-operations-smoke-test.js 8.5 KB

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