response-future-promise-smoke-test.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. 'use strict';
  2. const assert = require('assert/strict');
  3. const {
  4. FUTURE_PROMISE_REASON_CODES: REASONS,
  5. extractActionClaims,
  6. normalizeActionEvidence,
  7. verifyFuturePromises,
  8. } = require('../mcp/src/core/response-future-promise-verifier');
  9. let assertions = 0;
  10. function check(condition, message) {
  11. assertions += 1;
  12. assert(condition, message);
  13. }
  14. function equal(actual, expected, message) {
  15. assertions += 1;
  16. assert.equal(actual, expected, message);
  17. }
  18. function hasReason(result, reason) {
  19. check(result.reasonCodes.includes(reason), `${reason} missing: ${result.reasonCodes.join(',')}`);
  20. }
  21. function successfulTool(tool, result, extra = {}) {
  22. return { tool, status: 'completed', result, ...extra };
  23. }
  24. const noEvidence = verifyFuturePromises({ reply: '我稍后核实库存,再通知您。' });
  25. equal(noEvidence.passed, false, 'future promises without evidence must fail');
  26. equal(noEvidence.failedClaimIds.length, 2, 'verify and notify should be independent claims');
  27. hasReason(noEvidence, REASONS.FUTURE_ACTION_MISSING_EVIDENCE);
  28. const matchingEvidence = verifyFuturePromises({
  29. reply: '库存已经核实,结果已通知您。',
  30. toolTrace: [successfulTool('inventory_verify', { ok: true, subject: '库存', available: true })],
  31. outboundEvidence: { status: 'delivered', subject: '库存', result: { delivered: true } },
  32. });
  33. equal(matchingEvidence.passed, true, 'matching successful evidence should pass');
  34. equal(matchingEvidence.claims.length, 2);
  35. check(matchingEvidence.claims.every(claim => claim.evidenceMatched), 'all matching claims should carry evidence');
  36. check(matchingEvidence.claims.every(claim => claim.evidenceRefs.length === 1), 'evidence refs should be stable and compact');
  37. const unrelatedTool = verifyFuturePromises({
  38. reply: '库存已经核实。',
  39. toolTrace: [successfulTool('knowledge_search', { ok: true, subject: '品牌介绍' })],
  40. });
  41. equal(unrelatedTool.passed, false, 'unrelated tool result must not support stock verification');
  42. hasReason(unrelatedTool, REASONS.ACTION_EVIDENCE_UNRELATED);
  43. const citationBypass = verifyFuturePromises({
  44. reply: '我会稍后核实价格。',
  45. citations: [{ content: '价格说明' }],
  46. });
  47. equal(citationBypass.passed, false, 'citations are not execution evidence');
  48. hasReason(citationBypass, REASONS.FUTURE_ACTION_MISSING_EVIDENCE);
  49. const scopedUncertainty = verifyFuturePromises({ reply: '价格已经确认,其他信息待核验。' });
  50. equal(scopedUncertainty.passed, false, 'uncertainty in another clause must not protect a performed claim');
  51. hasReason(scopedUncertainty, REASONS.PERFORMED_ACTION_MISSING_EVIDENCE);
  52. const conditionalOffer = verifyFuturePromises({ reply: '可以为您核验,是否需要?' });
  53. equal(conditionalOffer.passed, true, 'conditional proposal is safe');
  54. equal(conditionalOffer.claims[0].requiresEvidence, false);
  55. hasReason(conditionalOffer, REASONS.SAFE_CONDITIONAL_OFFER);
  56. const missingInput = verifyFuturePromises({ reply: '请提供订单号,我再为您核实。' });
  57. equal(missingInput.passed, true, 'requesting missing input before continuing is safe');
  58. equal(missingInput.claims[0].requiresEvidence, false);
  59. hasReason(missingInput, REASONS.SAFE_MISSING_INPUT_REQUEST);
  60. const handoffMissing = verifyFuturePromises({ reply: '已经转交人工专员。' });
  61. equal(handoffMissing.passed, false, 'performed handoff requires evidence');
  62. equal(handoffMissing.claims[0].action, 'handoff');
  63. hasReason(handoffMissing, REASONS.PERFORMED_ACTION_MISSING_EVIDENCE);
  64. const handoffPassed = verifyFuturePromises({
  65. reply: '已经转交人工专员。',
  66. handoffEvidence: { status: 'completed', handoffId: 'handoff-test' },
  67. });
  68. equal(handoffPassed.passed, true, 'completed handoff evidence should pass');
  69. hasReason(handoffPassed, REASONS.PERFORMED_ACTION_VERIFIED);
  70. const outboundNotHandoff = verifyFuturePromises({
  71. reply: '已经转交人工专员。',
  72. outboundEvidence: { status: 'delivered', result: { delivered: true } },
  73. });
  74. equal(outboundNotHandoff.passed, false, 'outbound delivery cannot prove a handoff');
  75. hasReason(outboundNotHandoff, REASONS.ACTION_EVIDENCE_UNRELATED);
  76. const processingMissing = verifyFuturePromises({ reply: '正在处理您的退款。' });
  77. equal(processingMissing.passed, false, 'in-progress processing requires evidence');
  78. hasReason(processingMissing, REASONS.IN_PROGRESS_ACTION_MISSING_EVIDENCE);
  79. const activeCrossCheckMissing = verifyFuturePromises({ reply: '我现在核对执行条件,确认后回复您。' });
  80. equal(activeCrossCheckMissing.passed, false, 'first-person active cross-check requires evidence');
  81. hasReason(activeCrossCheckMissing, REASONS.IN_PROGRESS_ACTION_MISSING_EVIDENCE);
  82. const processingPassed = verifyFuturePromises({
  83. reply: '正在处理您的退款。',
  84. toolTrace: [successfulTool('process_refund', { ok: true, subject: '退款' })],
  85. });
  86. equal(processingPassed.passed, true, 'successful matching process evidence should pass');
  87. hasReason(processingPassed, REASONS.IN_PROGRESS_ACTION_VERIFIED);
  88. const failedEvidence = verifyFuturePromises({
  89. reply: '价格已经核实。',
  90. toolTrace: [{ tool: 'price_verify', status: 'failed', result: { ok: false, subject: '价格' } }],
  91. });
  92. equal(failedEvidence.passed, false, 'failed matching tool must not support a claim');
  93. hasReason(failedEvidence, REASONS.ACTION_EVIDENCE_FAILED);
  94. const callbackMissing = verifyFuturePromises({ reply: '我会尽快给您回电。' });
  95. equal(callbackMissing.passed, false, 'callback promise requires successful call evidence');
  96. equal(callbackMissing.claims[0].action, 'callback');
  97. const callbackPassed = verifyFuturePromises({
  98. reply: '已经给您回电。',
  99. toolTrace: [successfulTool('call_customer', { success: true, subject: '电话' })],
  100. });
  101. equal(callbackPassed.passed, true, 'call evidence should prove callback');
  102. const submitPassed = verifyFuturePromises({
  103. reply: '退款申请已经提交。',
  104. toolTrace: [successfulTool('submit_refund_request', { ok: true, subject: '退款申请', requestId: 'fixture' })],
  105. });
  106. equal(submitPassed.passed, true, 'matching submit evidence should prove submission');
  107. equal(submitPassed.claims.length, 2, 'refund and submit actions are independently evidenced by one matching tool');
  108. const cancelPassed = verifyFuturePromises({
  109. reply: '订单已经取消。',
  110. toolTrace: [successfulTool('cancel_order', { ok: true, subject: '订单' })],
  111. });
  112. equal(cancelPassed.passed, true, 'matching cancel evidence should pass');
  113. const safeNecessity = verifyFuturePromises({ reply: '当前价格需要核验。' });
  114. equal(safeNecessity.passed, true, 'honest necessity is not an execution claim');
  115. equal(safeNecessity.claims.length, 0);
  116. hasReason(safeNecessity, REASONS.NO_ACTION_CLAIMS);
  117. const noClaims = verifyFuturePromises({ reply: '当前资料不足,请提供订单号。' });
  118. equal(noClaims.passed, true);
  119. equal(noClaims.checks.length, 3);
  120. check(noClaims.checks.every(item => (
  121. typeof item.id === 'string'
  122. && typeof item.passed === 'boolean'
  123. && typeof item.weight === 'number'
  124. && typeof item.hard === 'boolean'
  125. && typeof item.detail === 'string'
  126. )), 'checks must keep the stable quality-check shape');
  127. const extracted = extractActionClaims('我稍后核实价格,再通知您。');
  128. equal(extracted.length, 2);
  129. check(extracted.every((claim, index) => claim.id === `claim_${index + 1}`), 'claim ids must be deterministic');
  130. const normalized = normalizeActionEvidence({
  131. handoffEvidence: { status: 'completed' },
  132. outboundEvidence: { delivered: true },
  133. });
  134. equal(normalized.length, 2);
  135. check(normalized.every(item => item.success), 'explicit success is required and preserved');
  136. check(normalized.some(item => item.actionIds.includes('handoff')));
  137. check(normalized.some(item => item.actionIds.includes('notify')));
  138. console.log(JSON.stringify({
  139. ok: true,
  140. assertions,
  141. cases: 21,
  142. checks: ['future_promise_evidence', 'performed_action_evidence', 'conditional_proposal_safety'],
  143. reasonCodes: Object.values(REASONS),
  144. }, null, 2));