import { describe, expect, it } from "vitest"; import { resolveAssistantOrchestrationDecision, resolveLivingAssistantModeDecision } from "../src/services/assistantService"; describe("assistant living router mode decision", () => { it("returns address_data when address lane already triggered", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "давай", addressLaneTriggered: true, useMock: false, predecomposeMode: "address_query", predecomposeModeConfidence: "high" }); expect(decision.mode).toBe("address_data"); expect(decision.reason).toBe("address_lane_triggered"); }); it("keeps deep pipeline in mock mode to avoid test-env network calls", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "привет", addressLaneTriggered: false, useMock: true, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("deep_analysis"); expect(decision.reason).toBe("mock_mode_keeps_deep_pipeline"); }); it("routes casual non-data phrase to chat mode", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "привет, как дела?", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); }); it("keeps deep mode for strong data signal", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "покажи документы по свк за 2020", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("deep_analysis"); expect(decision.reason).toBe("strong_data_signal_detected"); }); it("routes capability question to chat even when phrase contains 1С", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "и 1с можешь настроить?", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); expect(decision.reason).toBe("assistant_capability_query_detected"); }); it("routes capability question 'ok - what can you do in 1c' to chat", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "\u043e\u043a - \u0447\u0442\u043e \u043c\u043e\u0436\u0435\u0448\u044c \u043f\u043e 1\u0441", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); expect(decision.reason).toBe("assistant_capability_query_detected"); }); it("routes feature-capability wording to chat", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "а какие фичи по работе с 1с у тебя отработаны максимально?", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); expect(decision.reason).toBe("assistant_capability_query_detected"); }); it("routes data-scope question to chat instead of address lane", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "по какой компании мы можем работать?", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); expect(decision.reason).toBe("assistant_data_scope_query_detected"); }); it("routes 'whose base is this' style question to chat", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "ну база в тебе чья? как называется контора?", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); expect(decision.reason).toBe("assistant_data_scope_query_detected"); }); it("routes 'какая база подрублена?' to data-scope chat mode", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "какая база подрублена?", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); expect(decision.reason).toBe("assistant_data_scope_query_detected"); }); it("routes typo data-scope wording with misspelled company token to chat", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "подскажи плиз с какой компинией можем поработать?", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); expect(decision.reason).toBe("assistant_data_scope_query_detected"); }); it("routes data-scope wording without question mark when interrogative token is present", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "каза какой компании подключена к 1с", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("chat"); expect(decision.reason).toBe("assistant_data_scope_query_detected"); }); it("does not treat contract ranking data query as data-scope meta question", () => { const decision = resolveLivingAssistantModeDecision({ userMessage: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?", addressLaneTriggered: false, useMock: false, predecomposeMode: "unsupported", predecomposeModeConfidence: "low" }); expect(decision.mode).toBe("deep_analysis"); expect(decision.reason).toBe("strong_data_signal_detected"); }); }); describe("assistant orchestration contract", () => { it("keeps VAT payable forecast query in address lane", () => { const decision = resolveAssistantOrchestrationDecision({ rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020", effectiveAddressUserMessage: "какой прогноз оплаты ндс за 12 мая 2020", followupContext: null, llmPreDecomposeMeta: null, useMock: false }); expect(decision.runAddressLane).toBe(true); expect(decision.toolGateDecision).toBe("run_address_lane"); expect(decision.toolGateReason).toBe("address_mode_classifier_detected"); expect(decision.livingMode).toBe("address_data"); expect(decision.livingReason).toBe("address_lane_triggered"); expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false); }); it("keeps supported contract analytics query in address lane", () => { const decision = resolveAssistantOrchestrationDecision({ rawUserMessage: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?", effectiveAddressUserMessage: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?", followupContext: null, llmPreDecomposeMeta: null, useMock: false }); expect(decision.runAddressLane).toBe(true); expect(decision.toolGateDecision).toBe("run_address_lane"); expect(decision.toolGateReason).toBe("address_mode_classifier_detected"); expect(decision.livingMode).toBe("address_data"); expect(decision.livingReason).toBe("address_lane_triggered"); }); it("keeps VAT explain follow-up in address lane when followup context is present", () => { const decision = resolveAssistantOrchestrationDecision({ rawUserMessage: "почему прогноз к уплате 0?", effectiveAddressUserMessage: "почему прогноз к уплате 0?", followupContext: { previous_intent: "vat_payable_forecast", previous_filters: { period_from: "2020-03-01", period_to: "2020-03-31" }, previous_anchor_type: "unknown", previous_anchor_value: null }, llmPreDecomposeMeta: null, useMock: false }); expect(decision.runAddressLane).toBe(true); expect(decision.toolGateDecision).toBe("run_address_lane"); expect(decision.toolGateReason).toBe("followup_context_detected"); expect(decision.livingMode).toBe("address_data"); expect(decision.livingReason).toBe("address_lane_triggered"); }); it("keeps explicit address-mode unknown-intent data query in address lane", () => { const decision = resolveAssistantOrchestrationDecision({ rawUserMessage: "Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?", effectiveAddressUserMessage: "Показать контрагентов с вероятным несогласием между сальдо и актом сверки. Рекомендовать запросить сверку.", followupContext: null, llmPreDecomposeMeta: { applied: true, llmCanonicalCandidateDetected: true, predecomposeContract: { mode: "address_query", mode_confidence: "high", intent: "unknown", intent_confidence: "low" } } as any, useMock: false }); expect(decision.runAddressLane).toBe(true); expect(decision.toolGateDecision).toBe("run_address_lane"); expect(decision.livingMode).toBe("address_data"); expect(decision.livingReason).toBe("address_lane_triggered"); expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false); }); it("does not force address lane for deep-analysis unknown intent query with date-like token", () => { const decision = resolveAssistantOrchestrationDecision({ rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года", effectiveAddressUserMessage: "Найти ошибки в бухгалтерии за 21 мая 2022 года.", followupContext: null, llmPreDecomposeMeta: { applied: true, llmCanonicalCandidateDetected: true, predecomposeContract: { mode: "deep_analysis", mode_confidence: "high", intent: "unknown", intent_confidence: "low" } } as any, useMock: false }); expect(decision.runAddressLane).toBe(false); expect(decision.toolGateDecision).toBe("skip_address_lane"); expect(decision.livingMode).toBe("deep_analysis"); expect(["address_signal_unsupported_intent_fallback_to_deep", "no_address_signal_after_l0"]).toContain( decision.toolGateReason ); }); });