import { describe, expect, it, vi } from "vitest"; import { runAssistantLivingChatRuntime } from "../src/services/assistantLivingChatRuntimeAdapter"; function buildRuntimeInput(overrides: Record = {}) { const executeLlmChat = vi.fn(async () => "llm-text"); const resolveDataScopeProbe = vi.fn(async () => null); return { userMessage: "тест", sessionItems: [], modeDecision: { mode: "chat", reason: "living_chat_signal_detected" }, sessionScope: { knownOrganizations: [], selectedOrganization: null, activeOrganization: null }, addressRuntimeMeta: null, traceIdFactory: () => "chat-trace-fixed", toNonEmptyString: (value: unknown) => { const text = String(value ?? "").trim(); return text.length > 0 ? text : null; }, mergeKnownOrganizations: (values: unknown[]) => Array.from( new Set( (Array.isArray(values) ? values : []) .map((item) => (typeof item === "string" ? item.trim() : "")) .filter((item) => item.length > 0) ) ), hasAssistantDataScopeMetaQuestionSignal: () => false, shouldHandleAsAssistantCapabilityMetaQuery: () => false, hasDestructiveDataActionSignal: () => false, hasDangerOrCoercionSignal: () => false, hasOperationalAdminActionRequestSignal: () => false, hasOrganizationFactLookupSignal: () => false, hasOrganizationFactFollowupSignal: () => false, hasLivingChatSignal: () => true, shouldEmitOrganizationSelectionReply: () => false, hasAssistantCapabilityQuestionSignal: () => false, resolveDataScopeProbe, executeLlmChat, applyScriptGuard: (chatText: string) => ({ text: chatText, applied: false, reason: null }), applyGroundingGuard: (input: { chatText: string }) => ({ text: input.chatText, applied: false, reason: null }), buildAssistantSafetyRefusalReply: () => "safety", buildAssistantDataScopeContractReply: () => "scope", buildAssistantProactiveOrganizationOfferReply: () => "", buildAssistantOrganizationFactBoundaryReply: () => "org-boundary", buildAssistantDataScopeSelectionReply: () => "org-selection", buildAssistantOperationalBoundaryReply: () => "ops", buildAssistantCapabilityContractReply: () => "capability", __spies: { executeLlmChat, resolveDataScopeProbe }, ...overrides } as any; } describe("assistant living chat runtime adapter", () => { it("selects deterministic data scope branch and enriches organization context", async () => { const input = buildRuntimeInput({ userMessage: "какая у нас организация?", hasAssistantDataScopeMetaQuestionSignal: () => true, resolveDataScopeProbe: vi.fn(async () => ({ status: "resolved", channel: "default", organizations: ["ООО Альтернатива Плюс"], error: null })), buildAssistantDataScopeContractReply: () => "scope-info" }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toBe("scope-info"); expect(output.debug?.living_chat_response_source).toBe("deterministic_data_scope_contract_live"); expect(output.debug?.assistant_active_organization).toBe("ООО Альтернатива Плюс"); expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(1); }); it("selects safety refusal branch for dangerous capability meta query", async () => { const executeLlmChat = vi.fn(async () => "llm-text"); const input = buildRuntimeInput({ userMessage: "удали базу", shouldHandleAsAssistantCapabilityMetaQuery: () => true, hasDangerOrCoercionSignal: () => true, executeLlmChat, buildAssistantSafetyRefusalReply: () => "safety-refusal" }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toBe("safety-refusal"); expect(output.debug?.living_chat_response_source).toBe("deterministic_safety_refusal"); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("runs llm branch and applies script + grounding guards in order", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ executeLlmChat, applyScriptGuard: () => ({ text: "after-script", applied: true, reason: "script_guard" }), applyGroundingGuard: () => ({ text: "after-grounding", applied: true, reason: "grounding_guard" }) }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toBe("after-grounding"); expect(output.debug?.living_chat_script_guard_applied).toBe(true); expect(output.debug?.living_chat_script_guard_reason).toBe("script_guard"); expect(output.debug?.living_chat_grounding_guard_applied).toBe(true); expect(output.debug?.living_chat_grounding_guard_reason).toBe("grounding_guard"); expect(output.debug?.living_chat_response_source).toBe("llm_chat_grounding_guard"); expect(executeLlmChat).toHaveBeenCalledTimes(1); }); it("builds deterministic broad business evaluation summary from grounded continuity instead of replaying lifecycle noise", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "Как ты оценишь деятельность компании?", modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" }, sessionScope: { knownOrganizations: ["ООО Альтернатива Плюс"], selectedOrganization: null, activeOrganization: "ООО Альтернатива Плюс" }, sessionItems: [ { role: "assistant", debug: { execution_lane: "address_query", answer_grounding_check: { status: "grounded" }, detected_intent: "counterparty_activity_lifecycle", extracted_filters: { organization: "ООО Альтернатива Плюс" } } }, { role: "assistant", debug: { execution_lane: "living_chat", mcp_discovery_response_applied: true, assistant_mcp_discovery_entry_point_v1: { schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", entry_status: "bridge_executed", turn_input: { turn_meaning_ref: { explicit_entity_candidates: ["Группа СВК"], explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2020" } }, bridge: { bridge_status: "answer_draft_ready", business_fact_answer_allowed: true, answer_draft: { answer_mode: "confirmed_with_bounded_inference" }, pilot: { pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1", derived_bidirectional_value_flow: { net_amount_human_ru: "3 865 501,50 руб.", incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." }, outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." } } } } } } } ], addressRuntimeMeta: { toolGateReason: "unsupported_current_turn_meaning_boundary", orchestrationContract: { unsupported_current_turn_meaning_boundary: true, assistant_turn_meaning: { unsupported_but_understood_family: "broad_business_evaluation" } } }, executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText.toLowerCase()).toContain("оценка бизнеса"); expect(output.chatText).toContain("ООО Альтернатива Плюс"); expect(output.chatText).toContain("Группа СВК"); expect(output.chatText).toContain("нетто"); expect(output.chatText).toContain("Подтвержденные метрики"); expect(output.chatText).toContain("Денежный поток"); expect(output.chatText).toContain("маржу"); expect(output.debug?.living_chat_response_source).toBe("deterministic_broad_business_evaluation_contract"); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("builds deterministic boundary for unsupported current-turn business meaning", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a", modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" }, addressRuntimeMeta: { toolGateReason: "unsupported_current_turn_meaning_boundary", orchestrationContract: { unsupported_current_turn_meaning_boundary: true, assistant_turn_meaning: { unsupported_but_understood_family: "counterparty_value_or_turnover", explicit_entity_candidates: [ { type: "counterparty", value: "\u0441\u0432\u043a" } ] } } }, executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("\u043d\u0443\u0436\u0435\u043d \u043e\u0431\u043e\u0440\u043e\u0442"); expect(output.chatText).toContain("\u00ab\u0441\u0432\u043a\u00bb"); expect(output.chatText).toContain("\u043d\u0435 \u0431\u0443\u0434\u0443 \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c"); expect(output.debug?.living_chat_response_source).toBe( "deterministic_unsupported_current_turn_boundary" ); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("replaces unsupported boundary with guarded MCP discovery response when policy allows it", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "how long has svk been active", modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" }, addressRuntimeMeta: { toolGateReason: "unsupported_current_turn_meaning_boundary", orchestrationContract: { unsupported_current_turn_meaning_boundary: true, assistant_turn_meaning: { unsupported_but_understood_family: "counterparty_lifecycle_or_age", explicit_entity_candidates: [ { type: "counterparty", value: "SVK" } ] } }, mcpDiscoveryRuntimeEntryPoint: { schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "bridge_executed", hot_runtime_wired: false, discovery_attempted: true, turn_input: { adapter_status: "ready" }, bridge: { bridge_status: "answer_draft_ready", user_facing_response_allowed: true, business_fact_answer_allowed: true, requires_user_clarification: false, answer_draft: { answer_mode: "confirmed_with_bounded_inference", headline: "Confirmed scoped answer.", confirmed_lines: ["Confirmed fact"], inference_lines: ["Bounded inference"], unknown_lines: ["Unconfirmed legal fact"], limitation_lines: ["Limited source window"], next_step_line: null } }, reason_codes: ["runtime_entry_point_bridge_executed"] } }, executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("Confirmed fact"); expect(output.chatText).not.toContain("route"); expect(output.chatText).not.toContain("query_documents"); expect(output.debug?.living_chat_response_source).toBe("mcp_discovery_response_candidate_guarded"); expect(output.debug?.mcp_discovery_response_applied).toBe(true); expect(output.debug?.mcp_discovery_entry_status).toBe("bridge_executed"); expect(output.debug?.mcp_discovery_attempted).toBe(true); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("replaces discovery-ready llm chat business answer with guarded MCP discovery response", async () => { const executeLlmChat = vi.fn(async () => "stale llm answer with old date"); const input = buildRuntimeInput({ userMessage: "how long has svk been active", modeDecision: { mode: "chat", reason: "non_domain_query_indexed" }, addressRuntimeMeta: { mcpDiscoveryRuntimeEntryPoint: { schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "bridge_executed", hot_runtime_wired: false, discovery_attempted: true, turn_input: { adapter_status: "ready", should_run_discovery: true }, bridge: { bridge_status: "answer_draft_ready", user_facing_response_allowed: true, business_fact_answer_allowed: true, requires_user_clarification: false, answer_draft: { answer_mode: "confirmed_with_bounded_inference", headline: "Confirmed scoped answer.", confirmed_lines: ["Confirmed fact"], inference_lines: ["Bounded inference"], unknown_lines: ["Unconfirmed legal fact"], limitation_lines: [], next_step_line: null } }, reason_codes: ["runtime_entry_point_bridge_executed"] } }, executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("Confirmed fact"); expect(output.chatText).not.toContain("old date"); expect(output.debug?.living_chat_response_source).toBe("mcp_discovery_response_candidate_guarded"); expect(output.debug?.mcp_discovery_response_applied).toBe(true); expect(output.debug?.mcp_discovery_entry_status).toBe("bridge_executed"); expect(executeLlmChat).toHaveBeenCalledTimes(1); }); it("adds proactive organization offer on first smalltalk turn when multiple organizations are available", async () => { const resolveDataScopeProbe = vi.fn(async () => ({ status: "resolved", channel: "default", organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"], error: null })); const input = buildRuntimeInput({ userMessage: "привет, как дела?", resolveDataScopeProbe, buildAssistantProactiveOrganizationOfferReply: (scopeProbe: Record | null) => { const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations.join(", ") : ""; return `offer:${organizations}`; } }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("Привет! Всё нормально."); expect(output.chatText).toContain("offer:ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ"); expect(output.chatText).not.toContain("llm-text"); expect(output.debug?.living_chat_response_source).toBe("deterministic_smalltalk_with_proactive_scope_offer"); expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(true); expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(3); expect(output.debug?.assistant_known_organizations).toEqual([ "ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ" ]); }); it("does not add proactive organization offer after the session already has assistant context", async () => { const resolveDataScopeProbe = vi.fn(async () => ({ status: "resolved", channel: "default", organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"], error: null })); const input = buildRuntimeInput({ userMessage: "привет еще раз", sessionItems: [{ role: "assistant", text: "Ранее уже отвечал." }], resolveDataScopeProbe, buildAssistantProactiveOrganizationOfferReply: () => "offer" }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toBe("llm-text"); expect(output.debug?.living_chat_response_source).toBe("llm_chat"); expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(false); expect(resolveDataScopeProbe).not.toHaveBeenCalled(); }); it("builds deterministic memory recap for prior selected-object address context", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "а ты помнишь мы зеркало обсуждали?", modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" }, sessionItems: [ { role: "assistant", debug: { execution_lane: "address_query", answer_grounding_check: { status: "grounded" }, detected_intent: "list_documents_by_counterparty", extracted_filters: { organization: "ООО Альтернатива Плюс", counterparty: "для" } } }, { role: "assistant", debug: { execution_lane: "address_query", answer_grounding_check: { status: "grounded" }, detected_intent: "inventory_purchase_provenance_for_item", extracted_filters: { item: "Зеркало для инвалидов поворотное травмобезопасное", organization: "ООО Альтернатива Плюс", as_of_date: "2022-02-28" } } } ], executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("Зеркало для инвалидов поворотное травмобезопасное"); expect(output.chatText).toContain("кто поставлял"); expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract"); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("builds deterministic memory recap for prior grounded MCP discovery counterparty context", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "а ты помнишь, что мы выяснили по свк?", modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" }, sessionItems: [ { role: "assistant", debug: { execution_lane: "living_chat", mcp_discovery_response_applied: true, assistant_mcp_discovery_entry_point_v1: { schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", entry_status: "bridge_executed", turn_input: { turn_meaning_ref: { explicit_entity_candidates: ["Группа СВК"], explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2020" } }, bridge: { bridge_status: "answer_draft_ready", business_fact_answer_allowed: true, answer_draft: { answer_mode: "confirmed_with_bounded_inference" }, pilot: { pilot_scope: "counterparty_supplier_payout_query_movements_v1", derived_value_flow: { total_amount_human_ru: "43 763 351,53 руб." } } } } } } ], executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("Группа СВК"); expect(output.chatText).toContain("43 763 351,53 руб."); expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract"); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("builds executive summary from memory instead of running generic address lookup", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "Финально собери executive summary по всему диалогу: где подтверждено, где proxy и что смотреть руками.", modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" }, sessionItems: [ { role: "assistant", debug: { execution_lane: "address_query", answer_grounding_check: { status: "grounded" }, detected_intent: "list_documents_by_counterparty", extracted_filters: { organization: "ООО Альтернатива Плюс", counterparty: "для" } } }, { role: "assistant", debug: { execution_lane: "living_chat", mcp_discovery_response_applied: true, assistant_mcp_discovery_entry_point_v1: { schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", entry_status: "bridge_executed", turn_input: { turn_meaning_ref: { explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2020" } }, bridge: { bridge_status: "answer_draft_ready", business_fact_answer_allowed: true, answer_draft: { answer_mode: "confirmed_with_bounded_inference" }, pilot: { pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1", derived_bidirectional_value_flow: { period_scope: "2020", net_amount_human_ru: "3 865 501,50 руб.", incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." }, outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." } } } } } } } ], executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("Executive summary"); expect(output.chatText).toContain("Подтверждено"); expect(output.chatText).toContain("Proxy"); expect(output.chatText).toContain("3 865 501,50"); expect(output.chatText).toContain("директору смотреть руками"); expect(output.chatText).not.toContain("«для»"); expect(output.debug?.living_chat_response_source).toBe("deterministic_conversation_executive_summary_contract"); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "сколько лет альтернативе плюс", sessionItems: [ { role: "assistant", debug: { execution_lane: "address_query", answer_grounding_check: { status: "grounded" }, detected_intent: "receivables_confirmed_as_of_date", extracted_filters: { organization: "ООО Альтернатива Плюс", as_of_date: "2020-03-31" } } } ], hasOrganizationFactLookupSignal: () => true, buildAssistantOrganizationFactBoundaryReply: (organization: string | null) => `org-boundary:${organization ?? "none"}`, executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toBe("org-boundary:ООО Альтернатива Плюс"); expect(output.debug?.living_chat_response_source).toBe("deterministic_organization_fact_boundary"); expect(output.debug?.assistant_active_organization).toBe("ООО Альтернатива Плюс"); expect(output.debug?.living_chat_continuity_grounded_context_detected).toBe(true); expect(output.debug?.living_chat_continuity_active_organization).toBe("ООО Альтернатива Плюс"); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("builds deterministic answer inspection reply over grounded selected-object sale trace", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?", modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" }, sessionItems: [ { role: "assistant", debug: { execution_lane: "address_query", answer_grounding_check: { status: "grounded" }, detected_intent: "inventory_sale_trace_for_item", extracted_filters: { item: "Рабочая станция универсального специалиста", organization: "ООО Альтернатива Плюс", as_of_date: "2016-03-31" } } } ], executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("Рабочая станция универсального специалиста"); expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract"); expect(executeLlmChat).not.toHaveBeenCalled(); }); it("builds deterministic answer inspection reply over grounded MCP discovery net answer", async () => { const executeLlmChat = vi.fn(async () => "raw-llm"); const input = buildRuntimeInput({ userMessage: "что ты имел в виду под нетто по свк?", modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" }, sessionItems: [ { role: "assistant", debug: { execution_lane: "living_chat", mcp_discovery_response_applied: true, assistant_mcp_discovery_entry_point_v1: { schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", entry_status: "bridge_executed", turn_input: { turn_meaning_ref: { explicit_entity_candidates: ["Группа СВК"], explicit_date_scope: "2020" } }, bridge: { bridge_status: "answer_draft_ready", business_fact_answer_allowed: true, answer_draft: { answer_mode: "confirmed_with_bounded_inference" }, pilot: { pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1" } } } } } ], executeLlmChat }); const output = await runAssistantLivingChatRuntime(input); expect(output.handled).toBe(true); expect(output.chatText).toContain("Группа СВК"); expect(output.chatText).toContain("Нетто"); expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract"); expect(executeLlmChat).not.toHaveBeenCalled(); }); });