import { describe, expect, it, vi } from "vitest"; import { runAssistantAddressLaneResponseRuntime } from "../src/services/assistantAddressLaneResponseRuntimeAdapter"; describe("assistant address lane response runtime adapter", () => { it("builds debug payload and finalizes address turn", () => { const finalizeAddressTurn = vi.fn(() => ({ response: { ok: true } })); const runtime = runAssistantAddressLaneResponseRuntime({ sessionId: "asst-1", userMessage: "raw", effectiveAddressUserMessage: "canon", addressLane: { handled: true, reply_text: "answer", reply_type: "factual", debug: { extracted_filters: { organization: "ООО Ромашка" } } }, carryoverMeta: { followupContext: { previous_intent: "list_documents" } }, llmPreDecomposeMeta: { attempted: true }, knownOrganizations: ["ООО Ромашка", "ООО Лютик"], activeOrganization: "ООО Ромашка", sanitizeOutgoingAssistantText: (text) => String(text ?? "").trim(), buildAddressDebugPayload: (addressDebug) => ({ ...(addressDebug as Record) }), buildAddressFollowupOffer: () => ({ suggestion: "continue_previous" }), mergeKnownOrganizations: (items) => Array.from(new Set(items)), toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null), appendItem: () => {}, getSession: () => ({ session_id: "asst-1", updated_at: "", items: [], investigation_state: null } as any), persistSession: () => {}, cloneConversation: (items) => items, logEvent: () => {}, messageIdFactory: () => "msg-1", finalizeAddressTurn }); expect(finalizeAddressTurn).toHaveBeenCalledWith( expect.objectContaining({ assistantReply: "answer", replyType: "factual", llmPreDecomposeMeta: { attempted: true } }) ); expect(runtime.response).toEqual({ ok: true }); expect(runtime.debug).toEqual( expect.objectContaining({ assistant_known_organizations: ["ООО Ромашка", "ООО Лютик"], assistant_active_organization: "ООО Ромашка", address_followup_offer: { suggestion: "continue_previous" }, assistant_runtime_contract_v1: expect.objectContaining({ schema_version: "assistant_runtime_contracts_v1" }), assistant_truth_answer_policy_v1: expect.objectContaining({ schema_version: "assistant_truth_answer_policy_runtime_v1", policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter" }), coverage_gate_contract: expect.objectContaining({ schema_version: "assistant_truth_answer_policy_runtime_v1" }), answer_shape_contract: expect.objectContaining({ reply_type: "factual" }), assistant_state_transition_v1: expect.objectContaining({ schema_version: "assistant_state_transition_runtime_v1", state_owner: "assistantStateTransitionRuntimeAdapter" }), state_transition_contract: expect.objectContaining({ schema_version: "assistant_state_transition_runtime_v1" }), assistant_capability_binding_v1: expect.objectContaining({ schema_version: "assistant_capability_runtime_binding_v1", binding_owner: "assistantCapabilityRuntimeBindingAdapter" }), capability_binding_contract: expect.objectContaining({ schema_version: "assistant_capability_runtime_binding_v1" }), capability_binding_response_guard: expect.objectContaining({ schema_version: "assistant_capability_binding_response_guard_v1", applied: false }), assistant_mcp_discovery_entry_point_v1: null, mcp_discovery_attempted: false, mcp_discovery_hot_runtime_wired: false }) ); }); it("attaches MCP discovery summary from predecompose runtime meta without changing the reply", () => { const runtime = runAssistantAddressLaneResponseRuntime({ sessionId: "asst-mcp", userMessage: "raw", effectiveAddressUserMessage: "raw", addressLane: { handled: true, reply_text: "answer", reply_type: "partial_coverage", debug: {} }, llmPreDecomposeMeta: { 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" } }, reason_codes: ["runtime_entry_point_bridge_executed"] } }, knownOrganizations: [], activeOrganization: null, sanitizeOutgoingAssistantText: (text) => String(text ?? ""), buildAddressDebugPayload: () => ({}), buildAddressFollowupOffer: () => null, mergeKnownOrganizations: (items) => items, toNonEmptyString: () => null, appendItem: () => {}, getSession: () => ({ session_id: "asst-mcp", updated_at: "", items: [], investigation_state: null } as any), persistSession: () => {}, cloneConversation: (items) => items, logEvent: () => {}, messageIdFactory: () => "msg-mcp", finalizeAddressTurn: () => ({ response: { ok: true } }) }); expect(runtime.response).toEqual({ ok: true }); expect(runtime.debug).toEqual( expect.objectContaining({ mcp_discovery_entry_status: "bridge_executed", mcp_discovery_attempted: true, mcp_discovery_bridge_status: "answer_draft_ready", mcp_discovery_answer_mode: "confirmed_with_bounded_inference", mcp_discovery_business_fact_answer_allowed: true, mcp_discovery_hot_runtime_wired: false }) ); }); it("can replace a stale address exact answer with a guarded MCP discovery candidate", () => { const finalizeAddressTurn = vi.fn((input) => ({ response: { ok: true, assistant_reply: input.assistantReply, reply_type: input.replyType, debug: input.debug } })); const runtime = runAssistantAddressLaneResponseRuntime({ sessionId: "asst-mcp-address", userMessage: "сколько мы заплатили СВК за 2020?", effectiveAddressUserMessage: "сколько мы заплатили СВК за 2020?", addressLane: { handled: true, reply_text: "stale documents answer", reply_type: "factual", debug: {} }, llmPreDecomposeMeta: { 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: "Discovery payout answer", confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"], inference_lines: ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"], unknown_lines: ["Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"], limitation_lines: [], next_step_line: null } }, reason_codes: ["runtime_entry_point_bridge_executed"] } }, knownOrganizations: [], activeOrganization: null, sanitizeOutgoingAssistantText: (text) => String(text ?? ""), buildAddressDebugPayload: () => ({}), buildAddressFollowupOffer: () => null, mergeKnownOrganizations: (items) => items, toNonEmptyString: () => null, appendItem: () => {}, getSession: () => ({ session_id: "asst-mcp-address", updated_at: "", items: [], investigation_state: null } as any), persistSession: () => {}, cloneConversation: (items) => items, logEvent: () => {}, messageIdFactory: () => "msg-mcp-address", finalizeAddressTurn }); expect(finalizeAddressTurn).toHaveBeenCalledWith( expect.objectContaining({ assistantReply: expect.stringContaining("Discovery payout answer"), replyType: "partial_coverage", debug: expect.objectContaining({ mcp_discovery_response_applied: true }) }) ); expect(String((runtime.response as any).assistant_reply)).toContain("исходящих платежей/списаний"); }); it("aligns final route metadata when a business-overview discovery candidate replaces a stale exact reply", () => { const finalizeAddressTurn = vi.fn((input) => ({ response: { ok: true, assistant_reply: input.assistantReply, reply_type: input.replyType, debug: input.debug } })); const runtime = runAssistantAddressLaneResponseRuntime({ sessionId: "asst-business-overview-address", userMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.", effectiveAddressUserMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.", addressLane: { handled: true, reply_text: "Контрагент: Группа СВК. Найдено документов: 19.", reply_type: "factual", debug: { detected_intent: "list_documents_by_counterparty", selected_recipe: "address_documents_by_counterparty_v1", response_type: "FACTUAL_LIST" } }, llmPreDecomposeMeta: { 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, turn_meaning_ref: { asked_domain_family: "business_overview", asked_action_family: "broad_evaluation", explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2020", raw_message: "Собери короткий бизнес-итог по компании и отдельно по СВК." }, data_need_graph: { business_fact_family: "business_overview", reason_codes: ["data_need_graph_family_business_overview"] } }, bridge: { bridge_status: "answer_draft_ready", user_facing_response_allowed: true, business_fact_answer_allowed: true, requires_user_clarification: false, planner: { selected_chain_id: "business_overview" }, pilot: { pilot_scope: "business_overview_route_template_v1", derived_business_overview: { period_scope: "2020", incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." }, outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." }, net_amount_human_ru: "3 865 501,50 руб.", net_direction: "net_incoming" } }, answer_draft: { answer_mode: "confirmed_with_bounded_inference", headline: "Business overview answer", confirmed_lines: ["Confirmed company business overview facts"], inference_lines: [], unknown_lines: ["Pure profit is not proven"], limitation_lines: [], next_step_line: null } }, reason_codes: ["runtime_entry_point_bridge_executed"] } }, knownOrganizations: [], activeOrganization: null, sanitizeOutgoingAssistantText: (text) => String(text ?? ""), buildAddressDebugPayload: (debug) => ({ ...(debug as Record) }), buildAddressFollowupOffer: () => null, mergeKnownOrganizations: (items) => items, toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null), appendItem: () => {}, getSession: () => ({ session_id: "asst-business-overview-address", updated_at: "", items: [], investigation_state: null } as any), persistSession: () => {}, cloneConversation: (items) => items, logEvent: () => {}, messageIdFactory: () => "msg-business-overview-address", finalizeAddressTurn }); expect((runtime.response as any).debug).toEqual( expect.objectContaining({ mcp_discovery_response_applied: true, detected_intent: "business_overview", selected_recipe: "business_overview", mcp_discovery_effective_response_route: "business_overview" }) ); expect(String((runtime.response as any).assistant_reply)).toContain("47 628 853,03"); }); it("keeps comparison-scope proof augmentation business-facing for organization clarification", () => { const finalizeAddressTurn = vi.fn((input) => ({ response: { ok: true, assistant_reply: input.assistantReply, reply_type: input.replyType, debug: input.debug } })); const runtime = runAssistantAddressLaneResponseRuntime({ sessionId: "asst-comparison-proof", userMessage: "ООО Альтернатива Плюс", effectiveAddressUserMessage: "ООО Альтернатива Плюс", addressLane: { handled: true, reply_text: "Коротко: по компании Альтернатива Плюс подтвержден company-level денежный срез: входящие 285 819 547,57 руб.", reply_type: "partial_coverage", debug: {} }, llmPreDecomposeMeta: { mcpDiscoveryRuntimeEntryPoint: { schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "bridge_executed", hot_runtime_wired: true, discovery_attempted: true, turn_input: { turn_meaning_ref: { asked_domain_family: "business_overview", business_overview_separate_entity_candidates: ["Группа СВК"], explicit_organization_scope: "Альтернатива Плюс" } }, bridge: { bridge_status: "not_applicable", user_facing_response_allowed: false, business_fact_answer_allowed: false, requires_user_clarification: false }, reason_codes: [] } }, knownOrganizations: [], activeOrganization: null, sanitizeOutgoingAssistantText: (text) => String(text ?? ""), buildAddressDebugPayload: () => ({}), buildAddressFollowupOffer: () => null, mergeKnownOrganizations: (items) => items, toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null), appendItem: () => {}, getSession: () => ({ session_id: "asst-comparison-proof", updated_at: "", items: [], investigation_state: null, address_navigation_state: { session_context: { comparison_scope: { organization: { label: "Альтернатива Плюс" }, counterparty: { label: "Группа СВК" }, proof_bundles: { counterparty_value_flow_bundle: { counterparty: "Группа СВК", incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." }, outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." }, net_amount_human_ru: "18 523 839 руб.", net_direction: "net_incoming" }, counterparty_document_bundle: { document_count: 19 } } } } } }) as any, persistSession: () => {}, cloneConversation: (items) => items, logEvent: () => {}, messageIdFactory: () => "msg-comparison-proof", finalizeAddressTurn }); const reply = String((runtime.response as any).assistant_reply); expect(reply).toContain("по компании ООО Альтернатива Плюс"); expect(reply).toContain("получили 20 653 490 руб."); expect(reply).toContain("документы: 19"); expect(reply).not.toContain("company-level"); expect(reply).not.toContain("reusable bundle"); expect((runtime.response as any).debug.comparison_scope_response_augmentation_v1).toEqual( expect.objectContaining({ applied: true, organization: "ООО Альтернатива Плюс", counterparty: "Группа СВК", document_count: 19 }) ); }); it("keeps debug bounded to shadow contracts when optional enrichment is absent", () => { const runtime = runAssistantAddressLaneResponseRuntime({ sessionId: "asst-2", userMessage: "raw", effectiveAddressUserMessage: "raw", addressLane: { handled: true, reply_text: "answer", reply_type: "partial_coverage", debug: {} }, knownOrganizations: [], activeOrganization: null, sanitizeOutgoingAssistantText: (text) => String(text ?? ""), buildAddressDebugPayload: () => ({}), buildAddressFollowupOffer: () => null, mergeKnownOrganizations: (items) => items, toNonEmptyString: () => null, appendItem: () => {}, getSession: () => ({ session_id: "asst-2", updated_at: "", items: [], investigation_state: null } as any), persistSession: () => {}, cloneConversation: (items) => items, logEvent: () => {}, messageIdFactory: () => "msg-2", finalizeAddressTurn: () => ({ response: { ok: true } }) }); expect(runtime.debug).toEqual( expect.objectContaining({ assistant_runtime_contract_v1: expect.objectContaining({ transition_contract_id: null, capability_contract_id: null, truth_gate_contract_status: "unknown" }), assistant_truth_answer_policy_v1: expect.objectContaining({ schema_version: "assistant_truth_answer_policy_runtime_v1" }), coverage_gate_contract: expect.objectContaining({ coverage_status: "blocked", truth_mode: "unsupported" }), answer_shape_contract: expect.objectContaining({ answer_shape: "blocked_no_answer", reply_type: "partial_coverage" }), assistant_state_transition_v1: expect.objectContaining({ application_status: "unresolved", effective_carryover_depth: "none" }), transition_contract_id: null, capability_contract_id: null, truth_gate_contract_status: "unknown", carryover_eligibility: "none", state_transition_id: null, state_transition_status: "unresolved", effective_carryover_depth: "none", capability_binding_status: "not_applicable", capability_binding_action: "observe_only", capability_binding_violations: [], capability_binding_response_guard: expect.objectContaining({ applied: false, action: "observe_only" }) }) ); expect(runtime.response).toEqual({ ok: true }); }); it("guards blocked capability binding responses before finalizing the turn", () => { const finalizeAddressTurn = vi.fn(() => ({ response: { ok: true } })); runAssistantAddressLaneResponseRuntime({ sessionId: "asst-3", userMessage: "кто поставщик?", effectiveAddressUserMessage: "кто поставщик?", addressLane: { handled: true, reply_text: "unsafe factual answer", reply_type: "factual", debug: { capability_id: "inventory_inventory_purchase_provenance_for_item", limited_reason_category: "missing_anchor", missing_required_filters: ["item"] } }, knownOrganizations: [], activeOrganization: null, sanitizeOutgoingAssistantText: (text) => String(text ?? ""), buildAddressDebugPayload: (addressDebug) => ({ ...(addressDebug as Record) }), buildAddressFollowupOffer: () => null, mergeKnownOrganizations: (items) => items, toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null), appendItem: () => {}, getSession: () => ({ session_id: "asst-3", updated_at: "", items: [], investigation_state: null } as any), persistSession: () => {}, cloneConversation: (items) => items, logEvent: () => {}, messageIdFactory: () => "msg-3", finalizeAddressTurn }); expect(finalizeAddressTurn).toHaveBeenCalledWith( expect.objectContaining({ assistantReply: expect.stringContaining("Нужно уточнение"), replyType: "partial_coverage", debug: expect.objectContaining({ capability_binding_status: "blocked", capability_binding_action: "clarify", capability_binding_response_guard: expect.objectContaining({ applied: true, action: "clarify", guarded_reply_type: "partial_coverage" }) }) }) ); }); });