import assert from 'node:assert/strict'; import test from 'node:test'; import { aggregateChatIntentObservations, isRuleFallbackClassification, observationFromIntentRoutedData, RULE_FALLBACK_CONFIDENCE, RULE_FALLBACK_REASON, summarizeChatIntentObservation, } from './chat-intent-observation.mjs'; import { CHAT_INTENT_ROUTE, classifyWithRules } from './chat-intent-router.mjs'; test('isRuleFallbackClassification accepts explicit flag or legacy 0.72 reason', () => { assert.equal(isRuleFallbackClassification({ ruleFallback: true }), true); assert.equal(isRuleFallbackClassification({ source: 'rule', confidence: RULE_FALLBACK_CONFIDENCE, reason: RULE_FALLBACK_REASON, }), true); assert.equal(isRuleFallbackClassification({ source: 'llm', confidence: RULE_FALLBACK_CONFIDENCE, reason: RULE_FALLBACK_REASON, }), false); }); test('summarizeChatIntentObservation tags short text and shadow flips', () => { const observation = summarizeChatIntentObservation({ route: CHAT_INTENT_ROUTE.DIRECT_CHAT, source: 'rule', confidence: 0.72, reason: RULE_FALLBACK_REASON, ruleFallback: true, llmShadow: { route: CHAT_INTENT_ROUTE.AGENT, wouldChangeRoute: true }, }, { text: '帮我做', memoryRecall: false, }); assert.equal(observation.ruleFallback, true); assert.equal(observation.textLengthBucket, 'short_le6'); assert.equal(observation.wouldChangeRoute, true); assert.equal(observation.llmShadowRoute, CHAT_INTENT_ROUTE.AGENT); }); test('observationFromIntentRoutedData prefers stored observation', () => { const stored = { route: 'direct_chat', ruleFallback: true, textLengthBucket: 'long_gt12' }; assert.deepEqual(observationFromIntentRoutedData({ route: 'agent_orchestration', observation: stored, }), stored); const inferred = observationFromIntentRoutedData({ route: 'direct_chat', source: 'rule', confidence: 0.72, reason: RULE_FALLBACK_REASON, }); assert.equal(inferred.ruleFallback, true); }); test('aggregateChatIntentObservations computes stage A ratios', () => { const summary = aggregateChatIntentObservations({ routed: [ { route: 'direct_chat', source: 'rule', ruleFallback: true, textLengthBucket: 'long_gt12', memoryRecall: false, wouldChangeRoute: true, llmShadowRoute: 'agent_orchestration', }, { route: 'direct_chat', source: 'rule', ruleFallback: false, textLengthBucket: 'short_le6', memoryRecall: true, wouldChangeRoute: false, llmShadowRoute: null, }, { route: 'agent_orchestration', source: 'llm', ruleFallback: false, textLengthBucket: 'long_gt12', memoryRecall: false, wouldChangeRoute: false, llmShadowRoute: null, }, ], escalatedCount: 1, contextInjectedCount: 1, }); assert.equal(summary.total, 3); assert.equal(summary.ruleFallbackRatio, 0.3333); assert.equal(summary.escalatedCount, 1); assert.equal(summary.memoryRecallDirectRatio, 1); assert.equal(summary.wouldChangeRouteRatio, 1); }); test('classifyWithRules attaches stage A observation on 0.72 fallback', () => { const result = classifyWithRules({ text: '帮我比较一下英得尔和美的车载冰箱', sessionId: 'h5direct_abc', sessionMessageCount: 3, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.source, 'rule'); assert.equal(result.ruleFallback, true); assert.equal(result.observation.ruleFallback, true); assert.equal(result.observation.textLengthBucket, 'long_gt12'); assert.equal(result.observation.memoryRecall, false); }); test('classifyWithRules marks memory recall without treating it as 0.72 fallback', () => { const result = classifyWithRules({ text: '你记得我说想去哪儿吗', sessionId: 'h5direct_abc', sessionMessageCount: 3, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.observation.memoryRecall, true); assert.equal(result.observation.ruleFallback, false); });