agent-conversation-journey-smoke-test.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. 'use strict';
  2. const assert = require('assert/strict');
  3. const {
  4. APPLIED_ACTION_STATUSES,
  5. CONFIRMING_EVIDENCE_TYPES,
  6. createJourneyDefinition,
  7. createJourneyState,
  8. applyJourneyFacts,
  9. isStageComplete,
  10. planAppliedAction,
  11. executeAppliedAction,
  12. confirmAppliedAction,
  13. serializeJourneyState,
  14. buildConversationStateSnapshot,
  15. } = require('../mcp/src/core/agent-conversation-journey');
  16. function confirmation(type, id, at) {
  17. return { type, id, at, success: true };
  18. }
  19. function main() {
  20. const definition = createJourneyDefinition({
  21. id: 'generic-consultation',
  22. initialStageId: 'understand',
  23. stages: [
  24. { id: 'understand', requiredFacts: ['goal'] },
  25. { id: 'qualify', requiredFacts: ['constraints.budget', 'constraints.timeline'] },
  26. { id: 'recommend', requiredFacts: ['selection'] },
  27. { id: 'commit', requiredFacts: ['commitment'] },
  28. ],
  29. });
  30. assert.deepEqual(APPLIED_ACTION_STATUSES, ['planned', 'executed', 'confirmed']);
  31. assert.deepEqual(CONFIRMING_EVIDENCE_TYPES, ['send', 'tool', 'handoff']);
  32. const initial = createJourneyState(definition, { at: '2026-08-06T01:00:00.000Z' });
  33. const observed = applyJourneyFacts(definition, initial, {
  34. constraints: { timeline: 'soon', budget: 100 },
  35. goal: 'compare options',
  36. }, { at: '2026-08-06T01:01:00.000Z' });
  37. assert.equal(initial.facts.goal, undefined);
  38. assert.equal(observed.currentStageId, 'understand');
  39. assert.equal(isStageComplete(definition, observed, 'qualify'), true);
  40. const planned = planAppliedAction(definition, observed, {
  41. key: 'reply:message-1',
  42. kind: 'reply',
  43. transition: { mode: 'auto' },
  44. payload: { draftId: 'draft-1' },
  45. at: '2026-08-06T01:02:00.000Z',
  46. });
  47. assert.equal(planned.currentStageId, 'understand');
  48. assert.equal(planned.appliedActions[0].status, 'planned');
  49. const executed = executeAppliedAction(definition, planned, {
  50. key: 'reply:message-1',
  51. evidence: { type: 'send', id: 'attempt-1', success: false },
  52. at: '2026-08-06T01:03:00.000Z',
  53. });
  54. assert.equal(executed.currentStageId, 'understand');
  55. assert.equal(executed.appliedActions[0].status, 'executed');
  56. assert.throws(() => confirmAppliedAction(definition, executed, {
  57. key: 'reply:message-1',
  58. evidence: { type: 'draft', id: 'draft-1', success: true },
  59. }), /Unsupported action evidence type/);
  60. assert.throws(() => confirmAppliedAction(definition, executed, {
  61. key: 'reply:message-1',
  62. evidence: { type: 'send', id: 'message-1', success: false },
  63. }), /successful evidence/);
  64. const advanced = confirmAppliedAction(definition, executed, {
  65. key: 'reply:message-1',
  66. evidence: confirmation('send', 'message-1', '2026-08-06T01:04:00.000Z'),
  67. });
  68. assert.equal(advanced.currentStageId, 'recommend');
  69. assert.equal(advanced.appliedActions[0].status, 'confirmed');
  70. assert.deepEqual(advanced.appliedActions[0].transitionResult.skippedStageIds, ['qualify']);
  71. assert(advanced.stageHistory.some(entry => entry.kind === 'skip' && entry.stageId === 'qualify'));
  72. const backPlanned = planAppliedAction(definition, advanced, {
  73. key: 'handoff:requalify',
  74. kind: 'human_handoff',
  75. transition: { mode: 'back', targetStageId: 'qualify' },
  76. });
  77. const backed = confirmAppliedAction(definition, backPlanned, {
  78. key: 'handoff:requalify',
  79. evidence: confirmation('handoff', 'handoff-1', '2026-08-06T01:05:00.000Z'),
  80. });
  81. assert.equal(backed.currentStageId, 'qualify');
  82. assert.equal(backed.stageHistory.at(-1).kind, 'back');
  83. const reenterPlanned = planAppliedAction(definition, backed, {
  84. key: 'tool:refresh-qualification',
  85. kind: 'refresh',
  86. transition: { mode: 'reenter', targetStageId: 'qualify' },
  87. });
  88. const reentered = confirmAppliedAction(definition, reenterPlanned, {
  89. key: 'tool:refresh-qualification',
  90. evidence: confirmation('tool', 'tool-run-1', '2026-08-06T01:06:00.000Z'),
  91. });
  92. assert.equal(reentered.currentStageId, 'qualify');
  93. assert.equal(reentered.stageHistory.at(-1).kind, 'reenter');
  94. const skipPlanned = planAppliedAction(definition, reentered, {
  95. key: 'reply:direct-commit',
  96. kind: 'reply',
  97. transition: { mode: 'to', targetStageId: 'commit' },
  98. });
  99. const skipped = confirmAppliedAction(definition, skipPlanned, {
  100. key: 'reply:direct-commit',
  101. evidence: confirmation('send', 'message-2', '2026-08-06T01:07:00.000Z'),
  102. });
  103. assert.equal(skipped.currentStageId, 'commit');
  104. assert.deepEqual(skipped.appliedActions.at(-1).transitionResult.skippedStageIds, ['recommend']);
  105. const deduped = planAppliedAction(definition, skipped, {
  106. key: 'reply:direct-commit',
  107. kind: 'reply',
  108. transition: { mode: 'to', targetStageId: 'commit' },
  109. });
  110. assert.equal(deduped.appliedActions.length, skipped.appliedActions.length);
  111. assert.equal(deduped.sequence, skipped.sequence);
  112. assert.throws(() => planAppliedAction(definition, skipped, {
  113. key: 'reply:direct-commit',
  114. kind: 'different-action',
  115. transition: { mode: 'none' },
  116. }), /key conflict/);
  117. const serialized = serializeJourneyState(skipped);
  118. assert.equal(serialized, serializeJourneyState(JSON.parse(serialized)));
  119. const reordered = {
  120. ...skipped,
  121. facts: {
  122. goal: skipped.facts.goal,
  123. constraints: {
  124. budget: skipped.facts.constraints.budget,
  125. timeline: skipped.facts.constraints.timeline,
  126. },
  127. },
  128. };
  129. assert.equal(serialized, serializeJourneyState(reordered));
  130. assert(skipped.appliedActions.every(action => APPLIED_ACTION_STATUSES.includes(action.status)));
  131. const snapshot = buildConversationStateSnapshot({
  132. inboundContent: '预算 20 万,想预约明天下午到店',
  133. messages: [{ role: 'user', content: '想了解企业服务收费' }],
  134. profile: { budget: '20万' },
  135. });
  136. assert.equal(snapshot.knownConstraints.budget.source, 'conversation');
  137. assert.equal(snapshot.highIntent.active, true);
  138. assert.equal(snapshot.highIntent.kind, 'schedule_intake');
  139. assert(snapshot.knownFields.includes('budget'));
  140. assert(snapshot.knownFields.includes('appointmentIntent'));
  141. process.stdout.write(`${JSON.stringify({
  142. status: 'ok',
  143. checks: 30,
  144. finalStage: skipped.currentStageId,
  145. actions: skipped.appliedActions.length,
  146. historyEntries: skipped.stageHistory.length,
  147. snapshotFields: snapshot.knownFields,
  148. }, null, 2)}\n`);
  149. }
  150. try { main(); }
  151. catch (error) {
  152. process.stderr.write(`${error.stack || error.message}\n`);
  153. process.exit(1);
  154. }