agent-sales-move-planner-smoke-test.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. 'use strict';
  2. const assert = require('assert/strict');
  3. const {
  4. SALES_MOVES,
  5. AgentSalesMovePlanner,
  6. planSalesMove,
  7. } = require('../mcp/src/core/agent-sales-move-planner');
  8. function assertSinglePrimary(plan) {
  9. assert(SALES_MOVES.includes(plan.primaryMove), plan.primaryMove);
  10. assert.equal(plan.candidateMoves.filter(candidate => candidate.selected).length, 1);
  11. assert.equal(plan.candidateMoves.find(candidate => candidate.selected).move, plan.primaryMove);
  12. }
  13. function main() {
  14. assert.deepEqual(SALES_MOVES, [
  15. 'answer', 'acknowledge_objection', 'clarify', 'recommend', 'advance', 'handoff',
  16. ]);
  17. const scenarioPlans = [
  18. planSalesMove({ scenario: 'direct_question' }),
  19. planSalesMove({ scenario: 'objection' }),
  20. planSalesMove({ scenario: 'recommendation_ready' }),
  21. planSalesMove({ scenario: 'unknown', missingFields: ['goal'] }),
  22. planSalesMove({ scenario: 'continuation', knownFields: { goal: 'retention' } }),
  23. ];
  24. scenarioPlans.forEach(assertSinglePrimary);
  25. const knownField = planSalesMove({
  26. scenario: 'first_consultation',
  27. knownFields: { budget: 20 },
  28. missingFields: ['budget'],
  29. guidelines: [{
  30. id: 'collect-budget',
  31. condition: { scenario: 'first_consultation', missingFields: ['budget'] },
  32. action: 'clarify',
  33. stage: '*',
  34. priority: 100,
  35. criticality: 'high',
  36. track: true,
  37. }],
  38. });
  39. assert.equal(knownField.primaryMove, 'advance');
  40. assert.deepEqual(knownField.appliedGuidelines, []);
  41. const objection = planSalesMove({ scenario: 'objection', knownFields: ['goal'] });
  42. assert.equal(objection.primaryMove, 'acknowledge_objection');
  43. assert.equal(objection.reason.code, 'scenario_objection');
  44. const highRisk = planSalesMove({
  45. scenario: 'recommendation_ready',
  46. riskLevel: 'high',
  47. guidelines: [{
  48. id: 'recommend-now', condition: 'always', action: 'recommend', stage: '*',
  49. priority: 999, criticality: 'critical', track: true,
  50. }],
  51. });
  52. assert.equal(highRisk.primaryMove, 'handoff');
  53. assert.equal(highRisk.reason.code, 'high_risk_handoff');
  54. assert.equal(highRisk.appliedGuidelines[0].id, 'recommend-now');
  55. const stableTie = planSalesMove({
  56. scenario: 'continuation',
  57. guidelines: [
  58. { id: 'first', condition: 'always', action: 'recommend', priority: 10, criticality: 'normal', track: true },
  59. { id: 'second', condition: 'always', action: 'advance', priority: 10, criticality: 'normal', track: true },
  60. ],
  61. });
  62. assert.equal(stableTie.primaryMove, 'recommend');
  63. assert.deepEqual(stableTie.appliedGuidelines.map(item => item.id), ['first', 'second']);
  64. const completed = planSalesMove({
  65. scenario: 'continuation',
  66. completedActions: ['collect-goal'],
  67. missingFields: ['goal'],
  68. guidelines: [
  69. { id: 'collect-goal', condition: 'missing:goal', action: 'clarify', priority: 20, criticality: 'high', track: true },
  70. { id: 'offer-next-step', condition: 'always', action: 'advance', priority: 10, criticality: 'normal', track: true },
  71. ],
  72. });
  73. assert.equal(completed.primaryMove, 'advance');
  74. assert.deepEqual(completed.appliedGuidelines.map(item => item.id), ['offer-next-step']);
  75. const explicit = new AgentSalesMovePlanner({
  76. guidelines: [{
  77. id: 'stage-follow-up',
  78. condition: { scenario: 'continuation', knownFields: ['goal'], notCompletedActions: ['stage-follow-up'] },
  79. action: 'advance',
  80. stage: 'follow_up',
  81. priority: 30,
  82. criticality: 'medium',
  83. track: true,
  84. }],
  85. }).plan({ scenario: 'continuation', stage: 'follow_up', knownFields: ['goal'] });
  86. assert.equal(explicit.primaryMove, 'advance');
  87. assert.equal(explicit.reason.guidelineId, 'stage-follow-up');
  88. assert.deepEqual(explicit.appliedGuidelines[0].matchChecks.map(check => check.passed), [true, true, true]);
  89. const safeFallback = planSalesMove({
  90. scenario: 'unknown',
  91. guidelines: [{
  92. id: 'unknown-natural-language-rule', condition: 'customer seems interested', action: 'recommend',
  93. priority: 100, criticality: 'high', track: true,
  94. }],
  95. });
  96. assert.equal(safeFallback.primaryMove, 'answer');
  97. assert.equal(safeFallback.reason.code, 'safe_fallback');
  98. assert.deepEqual(safeFallback.appliedGuidelines, []);
  99. const incompleteRuleFallback = planSalesMove({
  100. scenario: 'unknown',
  101. guidelines: [
  102. { id: 'empty-object', condition: {}, action: 'recommend', priority: 100, criticality: 'high', track: true },
  103. { id: 'empty-array', condition: [], action: 'advance', priority: 90, criticality: 'high', track: true },
  104. ],
  105. });
  106. assert.equal(incompleteRuleFallback.primaryMove, 'answer');
  107. assert.deepEqual(incompleteRuleFallback.appliedGuidelines, []);
  108. const stageMismatch = planSalesMove({
  109. scenario: 'continuation',
  110. stage: 'stage-b',
  111. knownFields: ['goal'],
  112. guidelines: [{
  113. id: 'stage-a-only', condition: 'always', action: 'recommend', stage: 'stage-a',
  114. priority: 100, criticality: 'critical', track: true,
  115. }],
  116. });
  117. assert.equal(stageMismatch.primaryMove, 'advance');
  118. assert.deepEqual(stageMismatch.appliedGuidelines, []);
  119. const plans = [
  120. ...scenarioPlans, knownField, objection, highRisk, stableTie, completed, explicit, safeFallback,
  121. incompleteRuleFallback, stageMismatch,
  122. ];
  123. plans.forEach(assertSinglePrimary);
  124. process.stdout.write(`${JSON.stringify({ status: 'ok', checks: 11, plans: plans.length }, null, 2)}\n`);
  125. }
  126. try { main(); }
  127. catch (error) {
  128. process.stderr.write(`${error.stack || error.message}\n`);
  129. process.exit(1);
  130. }