import { describe, expect, it } from "vitest"; import { createAssistantRoutePolicy } from "../src/services/assistantRoutePolicy"; function toNonEmptyString(value: unknown): string | null { if (value === null || value === undefined) { return null; } const text = String(value).trim(); return text.length > 0 ? text : null; } function normalizeOrganizationScopeValue(value: unknown): string | null { const text = toNonEmptyString(value); if (!text) { return null; } return text.replace(/^"+|"+$/g, "").replace(/^'+|'+$/g, ""); } function buildPolicy(overrides: Record = {}) { return createAssistantRoutePolicy({ repairAddressMojibake: (text: string) => text, findLastGroundedAddressAnswerDebug: () => null, findLastOrganizationClarificationAddressDebug: () => null, mergeKnownOrganizations: (values: unknown[]) => Array.from( new Set( (Array.isArray(values) ? values : []) .map((item) => normalizeOrganizationScopeValue(item)) .filter((item): item is string => Boolean(item)) ) ), normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage: () => null, resolveMetaSignalSet: (input: { rawUserMessage?: string; repairedRawUserMessage?: string; effectiveAddressUserMessage?: string; repairedEffectiveAddressUserMessage?: string; }) => { const samples = [ input.rawUserMessage, input.repairedRawUserMessage, input.effectiveAddressUserMessage, input.repairedEffectiveAddressUserMessage ].join(" "); return { dataScopeMetaQuery: /РїРѕ какой компании|какая база|РїРѕ каким конторам/i.test(samples), capabilityMetaQuery: /что ты можешь|что ты умеешь/i.test(samples), metaAnswerFollowupSignal: /это РЅРѕСЂРј|что думаешь/i.test(samples) }; }, resolveHardMetaMode: (input: { dataScopeMetaQuery?: boolean; capabilityMetaQuery?: boolean; dataRetrievalSignal?: boolean; }) => input.dataScopeMetaQuery ? "data_scope" : input.capabilityMetaQuery && !input.dataRetrievalSignal ? "capability" : null, isMetaFollowupOverGroundedAnswer: () => false, hasDataRetrievalRequestSignal: () => false, hasOrganizationFactLookupSignal: () => false, hasOrganizationFactFollowupSignal: () => false, hasAggregateBusinessAnalyticsSignal: () => false, hasStandaloneAddressTopicSignal: () => false, hasOpenContractsAddressSignal: () => false, detectAddressQuestionMode: () => ({ mode: "unsupported", confidence: "low" }), resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" }), toNonEmptyString, hasStrictDeepInvestigationCue: () => false, hasStrongDataIntentSignal: () => false, hasAccountingSignal: () => false, hasDangerOrCoercionSignal: () => false, hasAddressFollowupContextSignal: () => false, hasShortDebtMirrorFollowupSignal: () => false, isInventorySelectedObjectIntent: (intent: unknown) => /inventory/i.test(String(intent ?? "")), hasShortInventoryObjectFollowupSignal: () => false, resolveRouteMemorySignals: () => ({ contextualHistoricalCapabilityFollowupDetected: false, contextualMemoryRecapFollowupDetected: false }), findLastAddressAssistantItem: () => null, resolveAddressToolGateDecision: () => ({ runAddressLane: false, decision: "skip_address_lane", reason: "no_address_signal_after_l0" }), hasSameDateAccountFollowupSignalForPredecompose: () => false, hasLooseAllTimeAddressLookupSignal: () => false, hasDeepAnalysisPreferenceSignal: () => false, hasDirectDeepAnalysisSignal: () => false, compactWhitespace: (text: string) => String(text ?? "").replace(/\s+/g, " ").trim(), hasDeepSessionContinuationSignal: () => false, resolveLivingAssistantModeDecision: (input: { addressLaneTriggered?: boolean }) => input.addressLaneTriggered ? { mode: "address_data", reason: "address_lane_triggered" } : { mode: "chat", reason: "living_chat_signal_detected" }, resolveProviderExecutionState: (input: { useMock?: unknown; llmPreDecomposeReason?: unknown }) => { const reason = String(input?.llmPreDecomposeReason ?? ""); return { provider_mode: Boolean(input?.useMock) ? "mock" : "unknown", normalized_provider: null, use_mock: Boolean(input?.useMock), llm_runtime_unavailable_detected: /missing api key|authentication|api key is missing/i.test(reason), living_mode_forced_deep: Boolean(input?.useMock), living_mode_forced_reason: Boolean(input?.useMock) ? "mock_mode_keeps_deep_pipeline" : null }; }, ...overrides }); } describe("assistantRoutePolicy", () => { it("routes data-scope meta question to chat contract", () => { const policy = buildPolicy(); const decision = policy.resolveAssistantOrchestrationDecision({ rawUserMessage: "РїРѕ какой компании РјС‹ можем работать?", effectiveAddressUserMessage: "РїРѕ какой компании РјС‹ можем работать?", followupContext: null, llmPreDecomposeMeta: null, useMock: false }); expect(decision.runAddressLane).toBe(false); expect(decision.toolGateReason).toBe("assistant_data_scope_query_detected"); expect(decision.livingMode).toBe("chat"); expect(decision.orchestrationContract?.hard_meta_mode).toBe("data_scope"); expect(decision.orchestrationContract?.provider_execution?.provider_mode).toBe("unknown"); }); it("keeps supported address intent in address lane", () => { const policy = buildPolicy({ detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }), resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "high" }), resolveAddressToolGateDecision: () => ({ runAddressLane: true, decision: "run_address_lane", reason: "address_mode_classifier_detected" }) }); const decision = policy.resolveAssistantOrchestrationDecision({ rawUserMessage: "какие товары сейчас лежат РЅР° складе", effectiveAddressUserMessage: "какие товары сейчас лежат РЅР° складе", followupContext: null, llmPreDecomposeMeta: null, useMock: false }); expect(decision.runAddressLane).toBe(true); expect(decision.toolGateReason).toBe("address_mode_classifier_detected"); expect(decision.livingMode).toBe("address_data"); expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true); }); it("routes memory recap follow-up over grounded answer to chat", () => { const policy = buildPolicy({ resolveRouteMemorySignals: () => ({ contextualHistoricalCapabilityFollowupDetected: false, contextualMemoryRecapFollowupDetected: true }), findLastGroundedAddressAnswerDebug: () => ({ execution_lane: "address_query" }) }); const decision = policy.resolveAssistantOrchestrationDecision({ rawUserMessage: "Р° ты помнишь что РјС‹ обсуждали?", effectiveAddressUserMessage: "Р° ты помнишь что РјС‹ обсуждали?", followupContext: null, llmPreDecomposeMeta: { applied: false, reason: "normalized_fragment_rejected_semantic_guard", predecomposeContract: { mode: "unsupported", mode_confidence: "low", intent: "unknown", intent_confidence: "low" } }, useMock: false }); expect(decision.runAddressLane).toBe(false); expect(decision.toolGateReason).toBe("memory_recap_followup_detected"); expect(decision.livingMode).toBe("chat"); expect(decision.livingReason).toBe("memory_recap_followup_detected"); }); it("routes organization fact lookup away from address lane even with follow-up context", () => { const policy = buildPolicy({ hasDataRetrievalRequestSignal: () => true, hasStrongDataIntentSignal: () => true, detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }), resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "low" }), hasOrganizationFactLookupSignal: (text: unknown) => /возраст.*альтернатива плюс/i.test(String(text ?? "")), resolveAddressToolGateDecision: () => ({ runAddressLane: true, decision: "run_address_lane", reason: "address_mode_classifier_detected" }) }); const decision = policy.resolveAssistantOrchestrationDecision({ rawUserMessage: "а какой возраст у Альтернативы Плюс?", effectiveAddressUserMessage: "Какой возраст у контрагента Альтернатива Плюс?", followupContext: { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { organization: "ООО \"Альтернатива Плюс\"", as_of_date: "2021-03-31" }, previous_anchor_type: "item", previous_anchor_value: "Модуль прямоугольный 1400*110*750" }, llmPreDecomposeMeta: null, useMock: false }); expect(decision.runAddressLane).toBe(false); expect(decision.toolGateReason).toBe("organization_fact_lookup_signal_detected"); expect(decision.livingMode).toBe("chat"); expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected"); }); it("routes explicit recap wording with selected-object phrasing to chat even when address-like cues exist", () => { const policy = buildPolicy({ hasStrongDataIntentSignal: () => true, hasDataRetrievalRequestSignal: () => true, resolveRouteMemorySignals: () => ({ contextualHistoricalCapabilityFollowupDetected: false, contextualMemoryRecapFollowupDetected: true }), findLastGroundedAddressAnswerDebug: () => ({ execution_lane: "address_query", detected_intent: "inventory_purchase_provenance_for_item" }) }); const decision = policy.resolveAssistantOrchestrationDecision({ rawUserMessage: "а ты помнишь, что мы по этой позиции уже выяснили?", effectiveAddressUserMessage: "а ты помнишь, что мы по этой позиции уже выяснили?", followupContext: null, llmPreDecomposeMeta: { applied: false, reason: "normalized_fragment_rejected_semantic_guard", predecomposeContract: { mode: "unsupported", mode_confidence: "low", intent: "unknown", intent_confidence: "low" } }, useMock: false }); expect(decision.runAddressLane).toBe(false); expect(decision.toolGateReason).toBe("memory_recap_followup_detected"); expect(decision.livingMode).toBe("chat"); expect(decision.livingReason).toBe("memory_recap_followup_detected"); }); it("does not force unsupported-intent fallback when predecompose runtime is unavailable", () => { const policy = buildPolicy({ hasStrongDataIntentSignal: () => true, hasDataRetrievalRequestSignal: () => true, resolveAddressToolGateDecision: () => ({ runAddressLane: true, decision: "run_address_lane", reason: "address_mode_classifier_detected" }) }); const decision = policy.resolveAssistantOrchestrationDecision({ rawUserMessage: "покажи документы по сверке", effectiveAddressUserMessage: "покажи документы по сверке", followupContext: { root_context_only: true }, llmPreDecomposeMeta: { reason: "error:OpenAI API key is missing", predecomposeContract: { mode: "unsupported", mode_confidence: "low", intent: "unknown", intent_confidence: "low" } }, useMock: false }); expect(decision.toolGateReason).toBe("address_mode_classifier_detected"); expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false); expect(decision.orchestrationContract?.provider_execution?.llm_runtime_unavailable_detected).toBe(true); }); });