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) => { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : 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("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("llm-text"); expect(output.chatText).toContain("offer:ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ"); expect(output.debug?.living_chat_response_source).toBe("llm_chat_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: "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(); }); });