import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAgentOrchestrationToUserMessage, buildRouterContext, buildAgentOrchestrationAgentText, buildRouterNormalizedDecision, CHAT_INTENT_ROUTE, classifyWithRules, coercePageDataSkill, coercePageGenerationSkill, createChatIntentRouter, createManagedChatIntentRouter, isNormalizedRouterDecisionEnabled, isNormalizedRouterDecisionShadow, resolveGatewayAgentSessionId, resolveLegacyRouteFromClassification, resolveNormalizedRouterDecisionMode, logRouterDecisionShadow, ROUTER_DECISION_ROUTE, ROUTER_DECISION_MODE, ROUTER_DECISION_SESSION_HINT, isRealtimeInfoQuestion, isRichChannelPageIntent, isRichPublishableContentIntent, coerceRealtimeWebSkill, resolveChatIntentRouterPolicy, } from './chat-intent-router.mjs'; test('classifyWithRules routes greetings to direct chat', () => { const result = classifyWithRules({ text: '你好', userMessage: { role: 'user', content: [{ type: 'text', text: '你好' }], metadata: { displayText: '你好' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.source, 'rule'); }); test('classifyWithRules routes page generation to agent orchestration', () => { const result = classifyWithRules({ text: '帮我做一个秋夜诗的 H5 页面', userMessage: { role: 'user', content: [{ type: 'text', text: '帮我做一个秋夜诗的 H5 页面' }], metadata: { displayText: '帮我做一个秋夜诗的 H5 页面' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'static-page-publish'); }); test('classifyWithRules routes page data collection to page-data-collect', () => { const text = '帮我在页面增加调查问卷,密码 888 后台查看每个用户提交记录,要用 sqlite'; const result = classifyWithRules({ text, userMessage: { role: 'user', content: [{ type: 'text', text }], metadata: { displayText: text }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'page-data-collect'); }); test('classifyWithRules routes ordinary household ledger language to page-data-collect', () => { const text = '帮我做一个家庭记账页面,我每天可以记录日期、收入、支出、分类和备注,日期默认当天。再做一个管理页面,可以查看所有记录和收支汇总。'; const result = classifyWithRules({ text, userMessage: { role: 'user', content: [{ type: 'text', text }], metadata: { displayText: text }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'page-data-collect'); }); test('classifyWithRules routes implicit sticky-note timeline apps to page-data-collect', () => { const text = '帮我设计一个便签提醒,可以写便签提交,时间轴来显示'; const result = classifyWithRules({ text, userMessage: { role: 'user', content: [{ type: 'text', text }], metadata: { displayText: text }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'page-data-collect'); }); test('coercePageDataSkill overrides static-page-publish when data intent is present', () => { const text = '帮我在这个页面增加调查问卷,密码 888 查看提交记录'; const coerced = coercePageDataSkill( { route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: 'static-page-publish', reason: '明确需要生成或发布页面/文件', agentBrief: '生成页面', }, text, { grantedSkills: ['page-data-collect', 'static-page-publish'] }, ); assert.equal(coerced.suggestedSkill, 'page-data-collect'); assert.match(coerced.reason, /页面数据交互意图/); }); test('coercePageGenerationSkill skips when page data intent is present', () => { const text = '帮我在页面增加调查问卷和后台入口'; const coerced = coercePageGenerationSkill( { route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: null, reason: '任务编排', agentBrief: '', }, text, { grantedSkills: ['static-page-publish', 'page-data-collect'] }, ); assert.notEqual(coerced.suggestedSkill, 'static-page-publish'); }); test('classifyWithRules routes implicit travel guide page requests to static-page-publish', () => { const result = classifyWithRules({ text: '苏州攻略页面', userMessage: { role: 'user', content: [{ type: 'text', text: '苏州攻略页面' }], metadata: { displayText: '苏州攻略页面' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'static-page-publish'); assert.match(result.agentBrief, /不要停在追问/); }); test('coercePageGenerationSkill replaces product-campaign-page for content pages', () => { const coerced = coercePageGenerationSkill( { route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: 'product-campaign-page', reason: '模型路由判定', agentBrief: '', confidence: 0.9, source: 'llm', }, '苏州攻略页面', { grantedSkills: ['static-page-publish', 'product-campaign-page'] }, ); assert.equal(coerced.suggestedSkill, 'static-page-publish'); assert.match(coerced.reason, /static-page-publish/); }); test('classifyWithRules routes themed article requests to agent orchestration', () => { const result = classifyWithRules({ text: '帮我写个主题文章', userMessage: { role: 'user', content: [{ type: 'text', text: '帮我写个主题文章' }], metadata: { displayText: '帮我写个主题文章' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'static-page-publish'); }); test('classifyWithRules routes rich service account content to static-page-publish', () => { const text = `以下是今天(2026年7月7日,星期二)国际上的主要热点新闻: 🌍 地缘政治与安全 - 霍尔木兹海峡油轮遇袭。 - 伊朗最高领袖哈梅内伊葬礼。 - 中国核潜艇在南太平洋试射远程导弹。 - 俄罗斯空袭基辅。 🇺🇸 美国 - Charlie Kirk 遇刺案预审听证。 - 特朗普财务披露。 - 最高法院允许德州实施应用商店年龄验证法。 ⚽ 体育 - 2026世界杯,比利时4-1淘汰美国,西班牙1-0绝杀葡萄牙。 🎤 娱乐 - Taylor Swift 与 Travis Kelce 在麦迪逊广场花园举行婚礼。 📉 财经 - 道指首次突破53,000点,AI 芯片热潮推动股市新高。 - Samsung Q2 利润飙升1,800%。 🌡️ 其他 - 新泽西高温致至少25人死亡。 - 古巴全国大停电。 请整理给服务号用户阅读。`; const result = classifyWithRules({ text, userMessage: { role: 'user', content: [{ type: 'text', text }], metadata: { displayText: text }, }, }); assert.equal(isRichChannelPageIntent(text), true); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'static-page-publish'); assert.match(result.reason, /结构化长内容/); }); test('isRichChannelPageIntent does not route short service account copy to page', () => { assert.equal(isRichChannelPageIntent('请整理给服务号用户阅读:今天 AI 新闻有哪些?'), false); }); test('classifyWithRules prefers pages for rich structured content meant for readers', () => { const text = `下面是活动资料,请整理成适合用户阅读和转发的内容: 活动概览 - 名称:城市夜跑季 - 时间:7月12日到7月20日 - 地点:滨江步道、中央公园、城市广场 亮点介绍 - 三条不同难度路线,适合亲子、入门和进阶跑者 - 每晚有补给站、音乐打卡点和城市灯光秀 - 完赛可领取电子证书和联名纪念徽章 报名信息 - 早鸟票 39 元 - 标准票 59 元 - 团队票 5 人起享 8 折 注意事项 - 需提前 30 分钟签到 - 建议穿反光装备 - 遇强降雨活动顺延 传播重点 - 强调轻松参与、城市夜景和朋友一起完成 - 内容需要清晰、有层次,适合用户直接阅读。`; const result = classifyWithRules({ text, userMessage: { role: 'user', content: [{ type: 'text', text }], metadata: { displayText: text }, }, }); assert.equal(isRichPublishableContentIntent(text), true); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'static-page-publish'); }); test('isRichPublishableContentIntent keeps plain long summaries out of page fast path', () => { const text = `请总结以下会议纪要: 产品进展 - 首页完成第一版视觉调整 - 登录链路还需要补充错误提示 - 分享页需要统一按钮文案 技术进展 - 后端接口完成基础联调 - 前端状态管理仍有重复请求 - 缓存策略需要继续评估 风险 - 设计稿还没有最终确认 - 部分接口缺少压测数据 - 发布窗口需要和运营确认 下一步 - 明天上午确认视觉改动 - 下午完成联调复测 - 周五前整理发布清单`; assert.equal(isRichPublishableContentIntent(text), false); }); test('classifyWithRules honors forceDeepReasoning bypass', () => { const result = classifyWithRules({ text: '你好', forceDeepReasoning: true, userMessage: { role: 'user', content: [{ type: 'text', text: '你好' }], }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.match(result.reason, /深度推理/); }); test('classifyWithRules keeps agent session continuation even when llm router flag is true', () => { const continued = classifyWithRules({ text: '可以', sessionId: '20260708_64', sessionMessageCount: 4, llmRouterEnabled: true, }); assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT); assert.match(continued.reason, /Agent 会话确认|延续已有 Agent 会话/); }); test('classifyWithRules keeps empty pre-created sessions eligible for llm routing', () => { const continued = classifyWithRules({ text: '继续聊聊', sessionId: '20260704_2', sessionMessageCount: 2, }); assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(continued.reason, '延续已有 Agent 会话'); const fresh = classifyWithRules({ text: '我是 John,你对我的记忆知道多少?', sessionId: '20260704_3', sessionMessageCount: 0, }); assert.equal(fresh.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.match(fresh.reason, /记忆/); }); test('classifyWithRules keeps memory recall on agent when session already active', () => { const result = classifyWithRules({ text: '你记得我说想去哪儿吗', sessionId: '20260704_9', sessionMessageCount: 4, userMessage: { role: 'user', content: [{ type: 'text', text: '你记得我说想去哪儿吗' }], metadata: { displayText: '你记得我说想去哪儿吗' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.match(result.reason, /延续已有 Agent 会话/); }); test('classifyWithRules bypasses llm router when user selected summarize skill', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls.push(1); return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x"}' }; }, }, memoryV2: { async resolve() { assert.fail('memory resolve should not run for explicit skill selection'); }, }, }); const prompt = '请总结以下内容,提炼核心结论、重点信息和可执行建议(条理清晰、中文输出):'; const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: `${prompt}这是待总结的正文` }], metadata: { displayText: `${prompt}这是待总结的正文`, memindRun: { selectedChatSkill: 'summarize' }, }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'rule'); assert.equal(result.reason, '用户已选择 skill'); assert.equal(llmCalls.length, 0); }); test('classifyWithRules bypasses llm router for explicit web skill prompt', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls.push(1); return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x"}' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '请使用 web 技能:帮我搜索并查阅相关资料(优先官方文档),并给出中文摘要与来源链接。我的问题是:今天 AI 新闻', }], metadata: { displayText: '今天 AI 新闻' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'web'); assert.equal(llmCalls.length, 0); }); test('createChatIntentRouter keeps continued sessions on agent without llm router', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, modelProviderKeyId: 'key-router', model: 'deepseek-chat', llmProviderService: { async createChatCompletion(payload) { llmCalls.push(payload); return { ok: false, message: 'router disabled' }; }, }, memoryV2: { async resolve() { return { source: 'legacy-conversation-memory', memories: [{ label: 'goal', text: '用户想去日本旅游' }] }; }, }, }); const result = await router.classify({ userId: 'user-john', sessionId: '20260704_10', sessionMessageCount: 2, userMessage: { role: 'user', content: [{ type: 'text', text: '我想去日本玩' }], metadata: { displayText: '我想去日本玩' }, }, }); assert.equal(llmCalls.length, 0); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'rule'); assert.match(result.reason, /延续已有 Agent 会话/); }); test('createChatIntentRouter uses agent fallback for general conversational analysis (skill-only)', async () => { const llmCalls = []; const resolveCalls = []; const router = createChatIntentRouter({ enabled: true, modelProviderKeyId: 'key-router', model: 'deepseek-chat', llmProviderService: { async createChatCompletion({ messages, providerKeyId, model, temperature }) { llmCalls.push({ messages, providerKeyId, model, temperature }); return { ok: false, message: 'router disabled' }; }, }, memoryV2: { async resolve(input) { resolveCalls.push(input); return { source: 'pgvector', memories: [{ label: 'preference', text: '用户偏好简洁回答,不喜欢冗长铺垫' }], activeGoals: ['完善春季促销活动页'], }; }, }, }); const result = await router.classify({ userId: 'user-1', userMessage: { role: 'user', content: [{ type: 'text', text: 'React 19 有哪些新特性?' }], metadata: { displayText: 'React 19 有哪些新特性?' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); assert.equal(resolveCalls.length, 0); assert.equal(llmCalls.length, 0); }); test('createChatIntentRouter routes memory recall through rules on fresh session', async () => { const resolveCalls = []; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { return { ok: false, message: 'router disabled' }; }, }, memoryV2: { async resolve(input) { resolveCalls.push(input); return { source: 'pgvector', memories: [{ label: 'goal', text: '用户想去日本旅游' }], }; }, }, }); const result = await router.classify({ userId: 'user-john', userMessage: { role: 'user', content: [{ type: 'text', text: '你记得我说想去哪儿吗?' }], metadata: { displayText: '你记得我说想去哪儿吗?' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.source, 'rule'); assert.equal(resolveCalls.length, 0); }); test('buildRouterContext trims rich Memory V2 resolve payload for routing', () => { const context = buildRouterContext({ source: 'letta', memories: [ { label: 'preference', text: '用户偏好简洁回答,不喜欢冗长铺垫,尤其在工程排障时希望先给结论。' }, { label: 'habit', text: '用户经常要求生成 H5 活动页并发布到 MindSpace。' }, ], semanticMemories: ['上次正在修改活动页标题样式'], behaviorSummary: '用户倾向先本地验证,再决定是否发布生产。额外句子不应进入轻量摘要。', activeGoals: ['完善春季促销活动页'], profile: { ignored: true }, }); assert.equal(context.source, 'letta'); assert.equal(context.itemsUsed, 5); assert.match(context.content, /相关记忆/); assert.match(context.content, /行为摘要/); assert.match(context.content, /进行中目标/); assert.doesNotMatch(context.content, /ignored/); }); test('createChatIntentRouter routes memory recall through rules when memory resolve fails', async () => { const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { return { ok: false, message: 'router disabled' }; }, }, memoryV2: { async resolve() { throw new Error('memory down'); }, }, logger: { warn() {} }, }); const result = await router.classify({ userId: 'user-1', userMessage: { role: 'user', content: [{ type: 'text', text: '你记得我之前说过什么吗?' }], metadata: { displayText: '你记得我之前说过什么吗?' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.source, 'rule'); }); test('createManagedChatIntentRouter hot-loads admin config and stays skill-only', async () => { const states = [ { fingerprint: 'off', overrides: { MEMIND_CHAT_LLM_ROUTER_ENABLED: '0', }, }, { fingerprint: 'on', overrides: { MEMIND_CHAT_LLM_ROUTER_ENABLED: '1', MEMIND_CHAT_ROUTER_MODEL_PROVIDER_KEY_ID: 'key-router', MEMIND_CHAT_ROUTER_MODEL: 'deepseek-chat', }, }, ]; const llmCalls = []; const router = createManagedChatIntentRouter({ configService: { async getRuntimeState() { return states[0]; }, }, llmProviderService: { async createChatCompletion(input) { llmCalls.push(input); return { ok: true, providerKeyId: input.providerKeyId, model: input.model, reply: JSON.stringify({ route: 'direct_chat', confidence: 0.9, reason: '纯文字问答', suggested_skill: null, agent_brief: '', }), }; }, }, }); assert.equal(await router.isEnabled(), true); states.shift(); assert.equal(await router.isEnabled(), true); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我概括一下这段材料的要点' }], metadata: { displayText: '帮我概括一下这段材料的要点' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); assert.equal(llmCalls.length, 0); }); test('createChatIntentRouter uses agent fallback when rules miss (skill-only)', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, minConfidence: 0.8, llmProviderService: { async createChatCompletion() { llmCalls.push(1); return { ok: true, reply: JSON.stringify({ route: 'direct_chat', confidence: 0.42, reason: '可能是简单问答', suggested_skill: null, agent_brief: '', }), }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我整理一下这段材料' }], metadata: { displayText: '帮我整理一下这段材料' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); assert.equal(llmCalls.length, 0); }); test('createChatIntentRouter falls back to agent when rules miss (skill-only)', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls.push(1); return { ok: false, message: 'router model unavailable' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我概括一下这段材料的要点' }], metadata: { displayText: '帮我概括一下这段材料的要点' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); assert.equal(llmCalls.length, 0); }); test('classifyWithRules routes memory recall to direct chat on fresh session', () => { const result = classifyWithRules({ text: '你记得我说想去哪儿吗', sessionId: '20260704_3', sessionMessageCount: 0, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.match(result.reason, /记忆/); }); test('createChatIntentRouter fast-paths news lookup to agent without LLM', async () => { let llmCalls = 0; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls += 1; return { ok: false, message: 'router model unavailable' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我查一下今天的 AI 新闻' }], metadata: { displayText: '帮我查一下今天的 AI 新闻' }, }, grantedSkills: ['web'], }); assert.equal(llmCalls, 0); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'rule'); }); test('applyAgentOrchestrationToUserMessage preserves displayText and adds task envelope', () => { const enriched = applyAgentOrchestrationToUserMessage( { role: 'user', content: [{ type: 'text', text: '帮我做一个页面' }], metadata: { displayText: '帮我做一个页面', userVisible: true }, }, { route: CHAT_INTENT_ROUTE.AGENT, confidence: 0.9, reason: '需要生成页面', suggestedSkill: 'static-page-publish', agentBrief: '生成 HTML 并发布', source: 'llm', }, { grantedSkills: ['static-page-publish'] }, ); assert.equal(enriched.metadata.displayText, '帮我做一个页面'); assert.match(enriched.content[0].text, /Memind 任务编排/); assert.match(enriched.content[0].text, /static-page-publish/); assert.match(enriched.content[0].text, /帮我做一个页面/); }); test('buildAgentOrchestrationAgentText includes execution brief', () => { const text = buildAgentOrchestrationAgentText({ displayText: '帮我搜索今天的新闻', classification: { reason: '需要实时搜索', suggestedSkill: 'web', agentBrief: '使用 web skill 搜索并整理来源', }, skillPrompt: '请使用 web 技能:', }); assert.match(text, /需要实时搜索/); assert.match(text, /帮我搜索今天的新闻/); assert.match(text, /请使用 web 技能/); }); test('classifyWithRules attaches normalized decision on every rule result', () => { const result = classifyWithRules({ text: '你好', userMessage: { role: 'user', content: [{ type: 'text', text: '你好' }], metadata: { displayText: '你好' }, }, }); assert.ok(result.decision); assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT); assert.equal(result.decision.mode, ROUTER_DECISION_MODE.SSE); assert.equal(result.decision.session_hint, ROUTER_DECISION_SESSION_HINT.NEW); }); test('buildRouterNormalizedDecision maps legacy routes and session hints', () => { const continued = buildRouterNormalizedDecision( { route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: 'web' }, { text: '继续', sessionId: '20260704_10', sessionMessageCount: 2, toolMode: 'chat', }, ); assert.equal(continued.route, ROUTER_DECISION_ROUTE.AGENT); assert.equal(continued.session_hint, ROUTER_DECISION_SESSION_HINT.REUSE); assert.ok(continued.flags.includes('selected_skill')); const freshAgentSession = buildRouterNormalizedDecision( { route: CHAT_INTENT_ROUTE.AGENT }, { text: '你好', sessionId: '20260704_11', sessionMessageCount: 0, }, ); assert.equal(freshAgentSession.session_hint, ROUTER_DECISION_SESSION_HINT.NEW); const memoryRecall = buildRouterNormalizedDecision( { route: CHAT_INTENT_ROUTE.DIRECT_CHAT }, { text: '你记得我说过什么吗' }, ); assert.equal(memoryRecall.route, ROUTER_DECISION_ROUTE.CHAT); assert.ok(memoryRecall.flags.includes('memory_recall')); }); test('resolveLegacyRouteFromClassification prefers decision when flag enabled', () => { const classification = { route: CHAT_INTENT_ROUTE.DIRECT_CHAT, decision: { route: ROUTER_DECISION_ROUTE.AGENT }, }; assert.equal( resolveLegacyRouteFromClassification(classification), CHAT_INTENT_ROUTE.DIRECT_CHAT, ); const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1'; try { assert.equal( resolveLegacyRouteFromClassification(classification), CHAT_INTENT_ROUTE.AGENT, ); assert.equal(isNormalizedRouterDecisionEnabled(), true); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('resolveGatewayAgentSessionId honors session_hint without breaking hard rules', () => { const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1'; try { assert.equal( resolveGatewayAgentSessionId({ agentSessionId: 'sess-1', classification: { decision: { session_hint: ROUTER_DECISION_SESSION_HINT.REUSE }, }, }), 'sess-1', ); assert.equal( resolveGatewayAgentSessionId({ agentSessionId: 'sess-1', classification: { decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW }, }, }), null, ); assert.equal( resolveGatewayAgentSessionId({ agentSessionId: 'h5direct_abc', classification: { decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW }, }, }), 'h5direct_abc', ); assert.equal( resolveGatewayAgentSessionId({ agentSessionId: 'sess-1', forceDeepReasoning: true, classification: { decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW }, }, }), 'sess-1', ); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('resolveNormalizedRouterDecisionMode supports off, shadow, and on', () => { const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; try { delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; assert.equal(resolveNormalizedRouterDecisionMode(), 'off'); process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow'; assert.equal(resolveNormalizedRouterDecisionMode(), 'shadow'); assert.equal(isNormalizedRouterDecisionShadow(), true); assert.equal(isNormalizedRouterDecisionEnabled(), false); process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1'; assert.equal(resolveNormalizedRouterDecisionMode(), 'on'); assert.equal(isNormalizedRouterDecisionEnabled(), true); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('shadow mode keeps legacy route for gateway decisions', () => { const classification = { route: CHAT_INTENT_ROUTE.DIRECT_CHAT, decision: { route: ROUTER_DECISION_ROUTE.AGENT }, }; const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow'; try { assert.equal(resolveLegacyRouteFromClassification(classification), CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(isNormalizedRouterDecisionEnabled(), false); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('classifyWithRules fast-paths bedtime story on fresh session', () => { const result = classifyWithRules({ text: '讲一个睡前故事吧', llmRouterEnabled: true, sessionId: '20260706_2', sessionMessageCount: 0, userMessage: { role: 'user', content: [{ type: 'text', text: '讲一个睡前故事吧' }], metadata: { displayText: '讲一个睡前故事吧' }, }, }); assert.equal(result?.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result?.source, 'rule'); assert.equal(result?.decision?.route, ROUTER_DECISION_ROUTE.CHAT); }); test('classifyWithRules fast-paths world cup standings to agent orchestration', () => { const text = '世界杯现在赛况如何'; assert.equal(isRealtimeInfoQuestion(text), true); const result = classifyWithRules({ text, llmRouterEnabled: true, userMessage: { role: 'user', content: [{ type: 'text', text }], metadata: { displayText: text }, }, }); assert.equal(result?.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result?.source, 'rule'); assert.equal(result?.suggestedSkill, 'web'); assert.match(result?.reason, /实时/); }); test('createChatIntentRouter fast-paths world cup without calling router LLM', async () => { let llmCalls = 0; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls += 1; throw new Error('router should not be called'); }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '世界杯现在赛况如何' }], metadata: { displayText: '世界杯现在赛况如何' }, }, grantedSkills: ['web'], }); assert.equal(llmCalls, 0); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'rule'); }); test('resolveChatIntentRouterPolicy coerces direct_chat fallback to agent', () => { const policy = resolveChatIntentRouterPolicy({ env: { MEMIND_CHAT_ROUTER_FALLBACK_ROUTE: 'direct_chat', }, }); assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.AGENT); }); test('coerceRealtimeWebSkill replaces search with web for world cup query', () => { const coerced = coerceRealtimeWebSkill( { route: CHAT_INTENT_ROUTE.AGENT, confidence: 0.9, reason: '用户询问实时赛况,需要搜索最新信息', suggestedSkill: 'search', source: 'llm', }, '世界杯现在赛况如何', { grantedSkills: ['web', 'search'] }, ); assert.equal(coerced.suggestedSkill, 'web'); assert.match(coerced.agentBrief, /web_search/); }); test('applyAgentOrchestrationToUserMessage uses web news prompt for realtime query', () => { const enriched = applyAgentOrchestrationToUserMessage( { role: 'user', content: [{ type: 'text', text: '世界杯现在赛况如何' }], metadata: { displayText: '世界杯现在赛况如何', userVisible: true }, }, coerceRealtimeWebSkill( { route: CHAT_INTENT_ROUTE.AGENT, confidence: 0.9, reason: '需要搜索实时资料', suggestedSkill: 'web', source: 'rule', }, '世界杯现在赛况如何', { grantedSkills: ['web'] }, ), { grantedSkills: ['web'] }, ); assert.match(enriched.content[0].text, /请使用 web 技能:先搜索今天\/最新相关的新闻与热点/); assert.doesNotMatch(enriched.content[0].text, /search 技能:帮我在工作区中查找代码或文件/); assert.match(enriched.content[0].text, /web_search/); }); test('createChatIntentRouter coerces LLM search route to web for realtime query', async () => { const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { return { ok: true, reply: JSON.stringify({ route: 'agent_orchestration', confidence: 0.92, reason: '用户询问实时赛况,需要搜索最新信息', suggested_skill: 'search', agent_brief: '搜索赛况', }), }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '世界杯现在赛况如何' }], metadata: { displayText: '世界杯现在赛况如何' }, }, grantedSkills: ['web', 'search'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.suggestedSkill, 'web'); assert.match(result.agentBrief, /web_search/); }); test('createChatIntentRouter timeout fallback prefers agent even when policy requests direct_chat', async () => { const router = createChatIntentRouter({ enabled: true, timeoutMs: 5, fallbackRoute: CHAT_INTENT_ROUTE.DIRECT_CHAT, llmProviderService: { async createChatCompletion() { await new Promise((resolve) => setTimeout(resolve, 20)); return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x","suggested_skill":null,"agent_brief":""}' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '帮我整理一下这段材料' }], metadata: { displayText: '帮我整理一下这段材料' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); }); test('agent memory shadow resolve is independently gated and never marked for injection', async () => { const calls = []; const router = createChatIntentRouter({ env: { MEMORY_AGENT_RESOLVE_ENABLED: '1', MEMORY_AGENT_INJECTION_MODE: 'shadow', MEMORY_AGENT_RESOLVE_LIMIT: '3', MEMORY_AGENT_RESOLVE_TIMEOUT_MS: '100', }, memoryV2: { async resolve(input) { calls.push(input); return { source: 'legacy-conversation-memory', memories: [{ label: 'preference', text: '喜欢完整方案' }] }; }, }, }); const result = await router.resolveAgentMemoryContext({ userId: 'u1', sessionId: 's1', text: '帮我设计一个商城', }); assert.equal(result.mode, 'shadow'); assert.equal(result.injectionEnabled, false); assert.equal(result.skipped, false); assert.equal(result.memories.length, 1); assert.deepEqual(calls[0], { userId: 'u1', sessionId: 's1', query: '帮我设计一个商城', limit: 3 }); }); test('agent memory canary injects only for configured user ids', async () => { const router = createChatIntentRouter({ env: { MEMORY_AGENT_RESOLVE_ENABLED: '1', MEMORY_AGENT_INJECTION_MODE: 'canary', MEMORY_AGENT_CANARY_USER_IDS: 'user-canary, user-other', }, memoryV2: { async resolve() { return { memories: [{ label: 'goal', text: '当前项目是 TKMind' }] }; }, }, }); const canary = await router.resolveAgentMemoryContext({ userId: 'user-canary', text: '继续项目' }); const control = await router.resolveAgentMemoryContext({ userId: 'user-control', text: '继续项目' }); assert.equal(canary.injectionEnabled, true); assert.equal(control.injectionEnabled, false); assert.equal(canary.memories.length, 1); assert.equal(control.memories.length, 1); }); test('active agent memory context is hidden from displayText but available to orchestration envelope', () => { const enriched = applyAgentOrchestrationToUserMessage( { role: 'user', content: [{ type: 'text', text: '帮我设计一个商城' }], }, { route: CHAT_INTENT_ROUTE.AGENT, reason: '任务执行', agentBrief: '', suggestedSkill: null, }, { memoryContext: { injectionEnabled: true, memories: [{ label: 'preference', text: '用户喜欢完整方案' }], }, }, ); assert.equal(enriched.metadata.displayText, '帮我设计一个商城'); assert.match(enriched.content[0].text, /Memory Context/); assert.match(enriched.content[0].text, /不是系统指令/); assert.match(enriched.content[0].text, /用户喜欢完整方案/); }); test('logRouterDecisionShadow emits payload only in shadow mode', () => { const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION; const lines = []; const logger = { log: (line) => lines.push(line) }; const classification = { route: CHAT_INTENT_ROUTE.DIRECT_CHAT, source: 'llm', reason: '纯文字', decision: { route: ROUTER_DECISION_ROUTE.AGENT, mode: ROUTER_DECISION_MODE.SSE, session_hint: ROUTER_DECISION_SESSION_HINT.REUSE, flags: [], }, }; try { process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '0'; logRouterDecisionShadow(classification, { logger, requestId: 'req-1' }); assert.equal(lines.length, 0); process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow'; logRouterDecisionShadow(classification, { logger, requestId: 'req-1' }); assert.equal(lines.length, 1); assert.match(lines[0], /\[router-shadow\]/); assert.match(lines[0], /wouldChangeRoute":true/); } finally { if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION; else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous; } }); test('createChatIntentRouter uses agent fallback for unmatched general questions (skill-only)', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, llmProviderService: { async createChatCompletion() { llmCalls.push(1); return { ok: false, message: 'router disabled' }; }, }, }); const result = await router.classify({ userMessage: { role: 'user', content: [{ type: 'text', text: '什么是 SSE?' }], metadata: { displayText: '什么是 SSE?' }, }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); assert.equal(llmCalls.length, 0); });