import { describe, expect, it, vi } from "vitest"; import { buildAssistantAddressOrchestrationRuntime } from "../src/services/assistantAddressOrchestrationRuntimeAdapter"; function buildInput(overrides: Record = {}) { const runAddressLlmPreDecompose = vi.fn(async () => ({ attempted: true, applied: true, effectiveMessage: "канон", reason: "normalized_fragment_applied" })); const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ followupContext: { id: "ctx" } })); const resolveAssistantOrchestrationDecision = vi.fn(() => ({ runAddressLane: true, livingMode: "deep_analysis", livingReason: "address_mode_classifier_detected", toolGateDecision: "run_address_lane", toolGateReason: "address_mode_classifier_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })); const buildAddressDialogContinuationContractV2 = vi.fn(() => ({ schema_version: "address_dialog_continuation_contract_v2" })); return { userMessage: "сырой вопрос", sessionItems: [], llmProvider: "openai", useMock: false, featureAddressLlmPredecomposeV1: true, runAddressLlmPreDecompose, buildAddressLlmPredecomposeContractV1: () => ({ schema_version: "address_llm_predecompose_contract_v1" }), sanitizeAddressMessageForFallback: () => "sanitized", toNonEmptyString: (value: unknown) => { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; }, resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision, buildAddressDialogContinuationContractV2, __spies: { runAddressLlmPreDecompose, resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision, buildAddressDialogContinuationContractV2 }, ...overrides } as any; } describe("assistant address orchestration runtime adapter", () => { it("uses llm predecompose payload when feature is enabled", async () => { const input = buildInput(); const output = await buildAssistantAddressOrchestrationRuntime(input); expect(output.addressPreDecompose.reason).toBe("normalized_fragment_applied"); expect(output.addressInputMessage).toBe("канон"); expect(output.orchestrationDecision.runAddressLane).toBe(true); expect(output.livingModeDecision.mode).toBe("deep_analysis"); expect(output.addressRuntimeMeta.toolGateDecision).toBe("run_address_lane"); expect(output.addressRuntimeMeta.routePolicyContract).toEqual( expect.objectContaining({ schema_version: "assistant_route_policy_runtime_v1", policy_owner: "assistantRoutePolicyRuntimeAdapter", living_mode: "deep_analysis", tool_gate_decision: "run_address_lane", has_followup_context: true, has_orchestration_contract: true }) ); expect(output.addressRuntimeMeta.dialogContinuationContract).toEqual({ schema_version: "address_dialog_continuation_contract_v2" }); expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual( expect.objectContaining({ schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", entry_status: "skipped_not_applicable", hot_runtime_wired: false }) ); expect(input.__spies.runAddressLlmPreDecompose).toHaveBeenCalledTimes(1); expect(input.__spies.resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(1); expect(input.__spies.resolveAssistantOrchestrationDecision).toHaveBeenCalledTimes(1); }); it("runs MCP discovery entry point from real orchestration context without changing the route decision", async () => { const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({ schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "bridge_executed", hot_runtime_wired: false, discovery_attempted: true })); const input = buildInput({ userMessage: "Сколько лет мы работаем с Группа СВК?", runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: false, effectiveMessage: "Сколько лет мы работаем с Группа СВК?", reason: "raw_kept", predecomposeContract: { entities: { counterparty: "Группа СВК" }, period: {} } })), resolveAssistantOrchestrationDecision: vi.fn(() => ({ runAddressLane: false, livingMode: "chat", livingReason: "unsupported_current_turn_meaning_boundary", toolGateDecision: "skip_address_lane", toolGateReason: "unsupported_current_turn_meaning_boundary", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1", assistant_turn_meaning: { schema_version: "assistant_turn_meaning_v1", asked_domain_family: "counterparty", asked_action_family: "counterparty_lifecycle", unsupported_but_understood_family: "counterparty_lifecycle" } } })), runMcpDiscoveryRuntimeEntryPoint }); const output = await buildAssistantAddressOrchestrationRuntime(input); expect(output.livingModeDecision).toEqual({ mode: "chat", reason: "unsupported_current_turn_meaning_boundary" }); expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual( expect.objectContaining({ entry_status: "bridge_executed", discovery_attempted: true, hot_runtime_wired: false }) ); expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith( expect.objectContaining({ userMessage: "Сколько лет мы работаем с Группа СВК?", effectiveMessage: "Сколько лет мы работаем с Группа СВК?", assistantTurnMeaning: expect.objectContaining({ unsupported_but_understood_family: "counterparty_lifecycle" }), predecomposeContract: expect.objectContaining({ entities: { counterparty: "Группа СВК" } }) }) ); }); it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => { const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({ schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "bridge_executed", hot_runtime_wired: false, discovery_attempted: true })); const input = buildInput({ userMessage: "Р° теперь Р·Р° 2021?", runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: false, effectiveMessage: "Р° теперь Р·Р° 2021?", reason: "raw_kept", predecomposeContract: { mode: "unsupported", intent: "unknown", period: { scope: "year", period_from: "2021-01-01", period_to: "2021-12-31", has_explicit_period: true } } })), resolveAddressFollowupCarryoverContext: vi.fn(() => ({ followupContext: { previous_intent: "supplier_payouts_profile", target_intent: "supplier_payouts_profile", previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1", previous_anchor_type: "counterparty", previous_anchor_value: "Группа РЎР’Рљ", previous_filters: { counterparty: "Группа РЎР’Рљ", organization: "РћРћРћ Альтернатива Плюс", period_from: "2020-01-01", period_to: "2020-12-31" } } })), resolveAssistantOrchestrationDecision: vi.fn(() => ({ runAddressLane: true, livingMode: "address_data", livingReason: "address_lane_triggered", toolGateDecision: "run_address_lane", toolGateReason: "followup_context_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1", assistant_turn_meaning: { schema_version: "assistant_turn_meaning_v1", raw_message: "Р° теперь Р·Р° 2021?", effective_message: "Р° теперь Р·Р° 2021?", explicit_entity_candidates: [] } } })), runMcpDiscoveryRuntimeEntryPoint }); const output = await buildAssistantAddressOrchestrationRuntime(input); expect(output.orchestrationDecision.runAddressLane).toBe(true); expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith( expect.objectContaining({ userMessage: "Р° теперь Р·Р° 2021?", effectiveMessage: "Р° теперь Р·Р° 2021?", followupContext: expect.objectContaining({ previous_intent: "supplier_payouts_profile", target_intent: "supplier_payouts_profile", previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1", previous_anchor_type: "counterparty", previous_anchor_value: "Группа РЎР’Рљ", previous_filters: expect.objectContaining({ counterparty: "Группа РЎР’Рљ", organization: "РћРћРћ Альтернатива Плюс", period_from: "2020-01-01", period_to: "2020-12-31" }) }) }) ); expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual( expect.objectContaining({ entry_status: "bridge_executed", discovery_attempted: true, hot_runtime_wired: false }) ); }); it("keeps address orchestration alive when MCP discovery entry point fails", async () => { const input = buildInput({ runMcpDiscoveryRuntimeEntryPoint: vi.fn(async () => { throw new Error("discovery transport failed"); }) }); const output = await buildAssistantAddressOrchestrationRuntime(input); expect(output.orchestrationDecision.runAddressLane).toBe(true); expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toBeNull(); expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPointError).toBe("discovery transport failed"); }); it("builds deterministic fallback predecompose payload when feature is disabled", async () => { const input = buildInput({ featureAddressLlmPredecomposeV1: false, llmProvider: "local", runAddressLlmPreDecompose: vi.fn(async () => { throw new Error("must not be called"); }), resolveAssistantOrchestrationDecision: vi.fn(() => ({ runAddressLane: false, livingMode: "chat", livingReason: "predecompose_unsupported_mode", toolGateDecision: "skip_address_lane", toolGateReason: "llm_predecompose_unsupported_mode", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })) }); const output = await buildAssistantAddressOrchestrationRuntime(input); expect(output.addressPreDecompose.attempted).toBe(false); expect(output.addressPreDecompose.applied).toBe(false); expect(output.addressPreDecompose.provider).toBe("local"); expect(output.addressPreDecompose.reason).toBe("disabled_by_feature_flag"); expect(output.addressPreDecompose.sanitizedUserMessage).toBe("sanitized"); expect(output.addressInputMessage).toBe("сырой вопрос"); expect(output.livingModeDecision.mode).toBe("chat"); expect(output.addressRuntimeMeta.toolGateDecision).toBe("skip_address_lane"); }); it("prefers raw short follow-up over unsupported llm rewrite when carryover context exists", async () => { const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ followupContext: { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { organization: "ООО \\Альтернатива Плюс\\", as_of_date: "2026-04-15" } } })); const resolveAssistantOrchestrationDecision = vi.fn(() => ({ runAddressLane: true, livingMode: "address_data", livingReason: "address_lane_triggered", toolGateDecision: "run_address_lane", toolGateReason: "followup_context_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })); const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({ schema_version: "address_llm_predecompose_contract_v1", source_message: sourceMessage, canonical_message: canonicalMessage, mode: canonicalMessage === sourceMessage ? "address_query" : "unsupported", intent: canonicalMessage === sourceMessage ? "inventory_on_hand_as_of_date" : "unknown" })); const output = await buildAssistantAddressOrchestrationRuntime( buildInput({ userMessage: "ахуен а на март 2020", runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: true, effectiveMessage: "что не так в бухгалтерии за март 2020 года?", reason: "normalized_fragment_applied", predecomposeContract: { mode: "unsupported", intent: "unknown" } })), buildAddressLlmPredecomposeContractV1, resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision }) ); expect(output.addressInputMessage).toBe("ахуен а на март 2020"); expect(output.addressPreDecompose.applied).toBe(false); expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite"); expect(output.addressPreDecompose.predecomposeContract).toEqual( expect.objectContaining({ canonical_message: "ахуен а на март 2020", mode: "address_query", intent: "inventory_on_hand_as_of_date" }) ); expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({ sourceMessage: "ахуен а на март 2020", canonicalMessage: "ахуен а на март 2020" }); expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2); expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith( expect.objectContaining({ rawUserMessage: "ахуен а на март 2020", effectiveAddressUserMessage: "ахуен а на март 2020" }) ); }); it("prefers raw inventory temporal root follow-up over account-balance canonical drift", async () => { const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ followupContext: { previous_intent: "inventory_purchase_provenance_for_item", previous_filters: { item: "Четки Пост (84*117)", organization: "ООО \\Альтернатива Плюс\\" }, previous_anchor_type: "item", previous_anchor_value: "Четки Пост (84*117)", root_intent: "inventory_on_hand_as_of_date", root_filters: { as_of_date: "2020-03-31", period_from: "2020-03-01", period_to: "2020-03-31", organization: "ООО \\Альтернатива Плюс\\" }, current_frame_kind: "inventory_drilldown" } })); const resolveAssistantOrchestrationDecision = vi.fn(() => ({ runAddressLane: true, livingMode: "address_data", livingReason: "address_lane_triggered", toolGateDecision: "run_address_lane", toolGateReason: "followup_context_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })); const buildAddressLlmPredecomposeContractV1 = vi.fn( ({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({ schema_version: "address_llm_predecompose_contract_v1", source_message: sourceMessage, canonical_message: canonicalMessage, mode: "address_query", intent: canonicalMessage === sourceMessage ? "inventory_on_hand_as_of_date" : "account_balance_snapshot" }) ); const rawMessage = "остатки на июль 2019"; const output = await buildAssistantAddressOrchestrationRuntime( buildInput({ userMessage: rawMessage, runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: true, effectiveMessage: "проверить остатки по счетам на июль 2019 года", reason: "normalized_fragment_applied", predecomposeContract: { mode: "address_query", intent: "account_balance_snapshot" } })), buildAddressLlmPredecomposeContractV1, resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision }) ); expect(output.addressInputMessage).toBe(rawMessage); expect(output.addressPreDecompose.applied).toBe(false); expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite"); expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({ sourceMessage: rawMessage, canonicalMessage: rawMessage }); expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2); expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith( expect.objectContaining({ rawUserMessage: rawMessage, effectiveAddressUserMessage: rawMessage }) ); }); it("prefers raw selected-object inventory action over generic canonical drift intent", async () => { const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ followupContext: { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { organization: "ООО \\Альтернатива Плюс\\", as_of_date: "2016-06-30", period_from: "2016-06-01", period_to: "2016-06-30" } } })); const resolveAssistantOrchestrationDecision = vi.fn(() => ({ runAddressLane: true, livingMode: "address_data", livingReason: "address_lane_triggered", toolGateDecision: "run_address_lane", toolGateReason: "address_mode_classifier_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })); const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({ schema_version: "address_llm_predecompose_contract_v1", source_message: sourceMessage, canonical_message: canonicalMessage, mode: "address_query", intent: "unknown" })); const rawMessage = 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": кому мы это продали в итоге'; const output = await buildAssistantAddressOrchestrationRuntime( buildInput({ userMessage: rawMessage, runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: true, effectiveMessage: "Определить контрагента, которому была реализована позиция «Рабочая станция универсального специалиста (индивидуальное изготовление)» по выбранному объекту", reason: "normalized_fragment_applied", predecomposeContract: { mode: "address_query", intent: "open_items_by_counterparty_or_contract", semantics: { selected_object_scope_detected: true } } })), buildAddressLlmPredecomposeContractV1, resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision }) ); expect(output.addressInputMessage).toBe(rawMessage); expect(output.addressPreDecompose.applied).toBe(false); expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite"); expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({ sourceMessage: rawMessage, canonicalMessage: rawMessage }); expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2); expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith( expect.objectContaining({ rawUserMessage: rawMessage, effectiveAddressUserMessage: rawMessage }) ); }); it("prefers raw selected-object sale-destination wording over generic canonical drift intent", async () => { const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ followupContext: { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { as_of_date: "2020-05-31", period_from: "2020-05-01", period_to: "2020-05-31" } } })); const resolveAssistantOrchestrationDecision = vi.fn(() => ({ runAddressLane: true, livingMode: "address_data", livingReason: "address_lane_triggered", toolGateDecision: "run_address_lane", toolGateReason: "address_mode_classifier_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })); const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({ schema_version: "address_llm_predecompose_contract_v1", source_message: sourceMessage, canonical_message: canonicalMessage, mode: "address_query", intent: "unknown" })); const rawMessage = 'По выбранному объекту "Пуф арий": куда мы продали эту позицию'; const output = await buildAssistantAddressOrchestrationRuntime( buildInput({ userMessage: rawMessage, runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: true, effectiveMessage: "Определить контрагента по реализации позиции «Пуф арий»", reason: "normalized_fragment_applied", predecomposeContract: { mode: "address_query", intent: "open_items_by_counterparty_or_contract", semantics: { selected_object_scope_detected: true } } })), buildAddressLlmPredecomposeContractV1, resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision }) ); expect(output.addressInputMessage).toBe(rawMessage); expect(output.addressPreDecompose.applied).toBe(false); expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite"); expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({ sourceMessage: rawMessage, canonicalMessage: rawMessage }); expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2); expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith( expect.objectContaining({ rawUserMessage: rawMessage, effectiveAddressUserMessage: rawMessage }) ); }); it("prefers raw selected-object delivery wording over generic canonical drift intent", async () => { const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ followupContext: { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { as_of_date: "2021-03-31", period_from: "2021-03-01", period_to: "2021-03-31" } } })); const resolveAssistantOrchestrationDecision = vi.fn(() => ({ runAddressLane: true, livingMode: "address_data", livingReason: "address_lane_triggered", toolGateDecision: "run_address_lane", toolGateReason: "address_mode_classifier_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })); const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({ schema_version: "address_llm_predecompose_contract_v1", source_message: sourceMessage, canonical_message: canonicalMessage, mode: "address_query", intent: "unknown" })); const rawMessage = 'По выбранному объекту "Кромка с клеем 33 дуб ниагара 137 м": кому мы это поставили'; const output = await buildAssistantAddressOrchestrationRuntime( buildInput({ userMessage: rawMessage, runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: true, effectiveMessage: "Покажи взаиморасчеты с контрагентом по выбранной позиции", reason: "normalized_fragment_applied", predecomposeContract: { mode: "address_query", intent: "bank_operations_by_counterparty", semantics: { selected_object_scope_detected: true } } })), buildAddressLlmPredecomposeContractV1, resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision }) ); expect(output.addressInputMessage).toBe(rawMessage); expect(output.addressPreDecompose.applied).toBe(false); expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite"); expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2); expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith( expect.objectContaining({ rawUserMessage: rawMessage, effectiveAddressUserMessage: rawMessage }) ); }); it("prefers raw selected-object profitability wording over customer revenue canonical drift", async () => { const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ followupContext: { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { as_of_date: "2020-05-31", period_from: "2020-05-01", period_to: "2020-05-31" } } })); const resolveAssistantOrchestrationDecision = vi.fn(() => ({ runAddressLane: true, livingMode: "address_data", livingReason: "address_lane_triggered", toolGateDecision: "run_address_lane", toolGateReason: "address_mode_classifier_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })); const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({ schema_version: "address_llm_predecompose_contract_v1", source_message: sourceMessage, canonical_message: canonicalMessage, mode: "address_query", intent: "unknown" })); const rawMessage = 'По выбранному объекту "Четки Пост (84*117)": а сколько денег мы заработали с продажжи этих четок'; const output = await buildAssistantAddressOrchestrationRuntime( buildInput({ userMessage: rawMessage, runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: true, effectiveMessage: "Покажи выручку по контрагенту по выбранной позиции", reason: "normalized_fragment_applied", predecomposeContract: { mode: "address_query", intent: "customer_revenue_and_payments", semantics: { selected_object_scope_detected: true } } })), buildAddressLlmPredecomposeContractV1, resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision }) ); expect(output.addressInputMessage).toBe(rawMessage); expect(output.addressPreDecompose.applied).toBe(false); expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite"); expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2); expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith( expect.objectContaining({ rawUserMessage: rawMessage, effectiveAddressUserMessage: rawMessage }) ); }); it("prefers raw same-date follow-up over canonical rewrite to current date", async () => { const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ followupContext: { previous_intent: "vat_payable_confirmed_as_of_date", previous_filters: { as_of_date: "2019-09-30", period_from: "2019-09-01", period_to: "2019-09-30" } } })); const resolveAssistantOrchestrationDecision = vi.fn(() => ({ runAddressLane: true, livingMode: "address_data", livingReason: "address_lane_triggered", toolGateDecision: "run_address_lane", toolGateReason: "followup_context_detected", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } })); const rawMessage = "какие остатки по складу на эту же дату"; const output = await buildAssistantAddressOrchestrationRuntime( buildInput({ userMessage: rawMessage, runAddressLlmPreDecompose: vi.fn(async () => ({ attempted: true, applied: true, effectiveMessage: "получить остатки по складу на текущую дату", reason: "normalized_fragment_applied", predecomposeContract: { mode: "address_query", intent: "inventory_on_hand_as_of_date" } })), resolveAddressFollowupCarryoverContext, resolveAssistantOrchestrationDecision }) ); expect(output.addressInputMessage).toBe(rawMessage); expect(output.addressPreDecompose.applied).toBe(false); expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite"); expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2); expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith( expect.objectContaining({ rawUserMessage: rawMessage, effectiveAddressUserMessage: rawMessage }) ); }); });