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, 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" }, ...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"); }); 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"); }); });