import { describe, expect, it, vi } from "vitest"; import { buildAssistantMcpDiscoveryAnswerDraft } from "../src/services/assistantMcpDiscoveryAnswerAdapter"; import { executeAssistantMcpDiscoveryPilot } from "../src/services/assistantMcpDiscoveryPilotExecutor"; import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner"; function buildDeps(rows: Array>, error: string | null = null) { return { executeAddressMcpQuery: vi.fn(async () => ({ fetched_rows: rows.length, matched_rows: error ? 0 : rows.length, raw_rows: rows, rows: error ? [] : rows, error })) }; } function buildSequentialDeps(results: Array<{ rows: Array>; error?: string | null }>) { const executeAddressMcpQuery = vi.fn(async () => { const next = results.shift() ?? { rows: [] }; const rows = next.rows; const error = next.error ?? null; return { fetched_rows: rows.length, matched_rows: error ? 0 : rows.length, raw_rows: rows, rows: error ? [] : rows, error }; }); return { executeAddressMcpQuery }; } function buildMetadataDeps(rows: Array>, error: string | null = null) { return { executeAddressMcpMetadata: vi.fn(async () => ({ fetched_rows: error ? 0 : rows.length, raw_rows: error ? [] : rows, rows: error ? [] : rows, error })) }; } describe("assistant MCP discovery answer adapter", () => { it("turns confirmed lifecycle evidence into a human-safe bounded answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.internal_mechanics_allowed).toBe(false); expect(draft.headline).toContain("подтвержденная активность"); expect(draft.confirmed_lines[0]).toContain("SVK"); expect(draft.inference_lines[0]).toContain("меньше месяца"); expect(draft.inference_lines.join("\n")).toContain("Первая найденная активность: 2020-01-15"); expect(draft.inference_lines.join("\n")).toContain("не юридически подтвержденный возраст регистрации"); expect(draft.unknown_lines).toContain("Legal registration date is not proven by this MCP discovery pilot"); expect(draft.must_not_claim).toContain("Do not present inferred activity duration as a formally confirmed legal fact."); expect(draft.reason_codes).toContain("answer_contains_unknown_fact_boundary"); }); it("uses checked-sources mode when MCP failed and avoids confirmed facts", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([], "MCP fetch failed: timeout")); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("checked_sources_only"); expect(draft.confirmed_lines).toEqual([]); expect(draft.limitation_lines).toContain("MCP fetch failed: timeout"); expect(draft.next_step_line).toContain("MCP"); expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty."); }); it("turns generic document evidence into a bounded document answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "documents", asked_action_family: "list_documents", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "document_evidence" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Doc1" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("документ"); expect(draft.confirmed_lines).toContain("1C document rows were found for counterparty SVK"); expect(draft.inference_lines).toContain( "Counterparty document evidence is limited to confirmed 1C document rows in the checked scope" ); expect(draft.unknown_lines).toContain("Full document history outside the checked period is not proven by this MCP discovery pilot"); expect(draft.must_not_claim).toContain("Do not claim full document history outside the checked period."); }); it("turns generic movement evidence into a bounded movement answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "movements", asked_action_family: "list_movements", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "movement_evidence" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Move1" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("движений"); expect(draft.confirmed_lines).toContain("1C movement rows were found for counterparty SVK"); expect(draft.inference_lines).toContain( "Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope" ); expect(draft.unknown_lines).toContain("Full movement history outside the checked period is not proven by this MCP discovery pilot"); expect(draft.must_not_claim).toContain("Do not claim full movement history outside the checked period."); expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe."); }); it("asks for clarification when discovery did not execute due to missing scope", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toBe("Нужно уточнить контекст перед поиском в 1С."); expect(draft.next_step_line).toContain("Уточните контрагента"); expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false."); }); it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "metadata", asked_action_family: "resolve_next_lane", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "metadata_lane_choice_clarification" } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain("data-lane"); expect(draft.next_step_line).toContain("по документам"); expect(draft.next_step_line).toContain("по движениям/регистрам"); expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false."); }); it("turns metadata surface evidence into a human-safe metadata answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "metadata", asked_action_family: "inspect_documents", explicit_entity_candidates: ["НДС"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildMetadataDeps([ { FullName: "Документ.СчетФактураВыданный", MetaType: "Документ", attributes: [{ Name: "Дата" }, { Name: "Организация" }] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("заземлена вероятная поверхность"); expect(confirmedText).toContain("Подтвержденная metadata-поверхность 1С"); expect(confirmedText).toContain("Документ.СчетФактураВыданный"); expect(confirmedText).toContain("Выбранное family: Документ"); expect(confirmedText).toContain("Дата"); expect(draft.inference_lines.join("\n")).toContain("контур документов"); expect(draft.next_step_line).toContain("surface «Документ»"); expect(draft.must_not_claim).toContain("Do not present metadata surface as confirmed business data rows."); expect(draft.must_not_claim).toContain("Do not present the inferred next checked lane as already executed data retrieval."); }); it("keeps metadata answer honest when schema surface stays ambiguous", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "metadata", asked_action_family: "inspect_fields", explicit_entity_candidates: ["НДС"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildMetadataDeps([ { FullName: "Документ.СчетФактураВыданный", MetaType: "Документ", attributes: [{ Name: "Дата" }] }, { FullName: "РегистрНакопления.НДСПокупок", MetaType: "РегистрНакопления", resources: [{ Name: "СуммаНДС" }] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.headline).toContain("конкурирующие schema-поверхности"); expect(draft.inference_lines.join("\n")).toContain("несколько конкурирующих family"); expect(draft.unknown_lines).toContain( "Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления" ); expect(draft.next_step_line).toContain("Документ, РегистрНакопления"); }); it("turns value-flow evidence into a bounded turnover answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK" }, { Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("денежных движений"); expect(confirmedText).toContain("3 750,50 руб."); expect(confirmedText).toContain("2020-01-15"); expect(confirmedText).toContain("2020-02-20"); expect(draft.unknown_lines).toContain("Full turnover outside the checked period is not proven by this MCP discovery pilot"); expect(draft.must_not_claim).toContain("Do not claim full all-time turnover unless the checked period and coverage prove it."); expect(draft.limitation_lines.join("\n")).not.toContain("pilot_"); }); it("turns supplier payout evidence into a bounded outgoing payment answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "payout", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_payouts_or_outflow" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Period: "2020-03-15T00:00:00", Amount: 4100, Counterparty: "SVK" }, { Period: "2020-04-20T00:00:00", Amount: "900,25", Counterparty: "SVK" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("исходящих платежей"); expect(confirmedText).toContain("исходящих платежей/списаний"); expect(confirmedText).toContain("5 000,25 руб."); expect(draft.inference_lines.join("\n")).toContain("supplier-payout total"); expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"); }); it("turns bidirectional value-flow evidence into a bounded net cash answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "net_value_flow", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([ { rows: [ { Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" }, { Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" } ] }, { rows: [{ Period: "2020-03-10T00:00:00", Amount: 4000, Counterparty: "SVK" }] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("входящих и исходящих денежных движений"); expect(confirmedText).toContain("получили 12 500,50 руб."); expect(confirmedText).toContain("заплатили 4 000 руб."); expect(confirmedText).toContain("нетто в нашу сторону: 8 500,50 руб."); expect(draft.inference_lines.join("\n")).toContain("net value-flow"); expect(draft.unknown_lines).toContain("Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"); expect(draft.must_not_claim).toContain("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows."); }); it("renders monthly bidirectional breakdown lines when the turn explicitly asked by month", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "net_value_flow", asked_aggregation_axis: "month", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([ { rows: [ { Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" }, { Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" } ] }, { rows: [ { Period: "2020-01-10T00:00:00", Amount: 4000, Counterparty: "SVK" }, { Period: "2020-02-11T00:00:00", Amount: 1000, Counterparty: "SVK" } ] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("помесяч"); expect(confirmedText).toContain("Помесячно: янв 2020"); expect(confirmedText).toContain("получили 10 000 руб."); expect(confirmedText).toContain("заплатили 4 000 руб."); expect(confirmedText).toContain("Помесячно: фев 2020"); expect(confirmedText).toContain("нетто в нашу сторону 1 500,50 руб."); expect(draft.reason_codes).toContain("answer_contains_monthly_breakdown"); }); it("keeps recovered yearly coverage out of the unknown block and explains the recovery as bounded inference", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "payout", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_payouts_or_outflow" } }); const broadRows = Array.from({ length: 100 }, (_, index) => ({ Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`, Amount: 10, Counterparty: "SVK" })); const monthlyResults = Array.from({ length: 12 }, (_, index) => ({ rows: [ { Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`, Amount: (index + 1) * 100, Counterparty: "SVK" } ] })); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([{ rows: broadRows }, ...monthlyResults]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.inference_lines).toContain( "Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit" ); expect(draft.unknown_lines).not.toContain( "Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached" ); }); it("does not leak primitive names or query text into user-facing lines", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const userFacing = [ draft.headline, ...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines, ...draft.limitation_lines, draft.next_step_line ?? "" ].join("\n"); expect(userFacing).not.toContain("query_documents"); expect(userFacing).not.toContain("SELECT"); expect(userFacing).not.toContain("ВЫБРАТЬ"); expect(userFacing).not.toContain("primitive"); }); it("verbalizes activity duration from first and latest confirmed 1C rows", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Period: "2020-01-15T00:00:00", Counterparty: "SVK" }, { Period: "2023-12-20T00:00:00", Counterparty: "SVK" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const inferenceText = draft.inference_lines.join("\n"); expect(inferenceText).toContain("3 года 11 месяцев"); expect(inferenceText).toContain("2020-01-15"); expect(inferenceText).toContain("2023-12-20"); expect(inferenceText).toContain("не юридически подтвержденный возраст регистрации"); expect(draft.reason_codes).toContain("pilot_derived_activity_period_from_confirmed_rows"); }); });