import { afterEach, describe, expect, it, vi } from "vitest"; const { executeAddressMcpQueryMock } = vi.hoisted(() => ({ executeAddressMcpQueryMock: vi.fn() })); vi.mock("../src/services/addressMcpClient", async () => { const actual = await vi.importActual( "../src/services/addressMcpClient" ); return { ...actual, executeAddressMcpQuery: executeAddressMcpQueryMock }; }); import { resolveAddressIntent } from "../src/services/addressIntentResolver"; import { AddressQueryService } from "../src/services/addressQueryService"; import { composeFactualReply } from "../src/services/address_runtime/composeStage"; afterEach(() => { executeAddressMcpQueryMock.mockReset(); vi.restoreAllMocks(); }); describe("counterparty shipment item flow and open-items routing", () => { it("routes counterparty shipment item-flow wording to documents by counterparty", () => { const result = resolveAddressIntent("что нам отгружал чепурнов? какой товар или услугу?"); expect(result.intent).toBe("list_documents_by_counterparty"); expect(result.reasons).toContain("counterparty_item_flow_signal_detected"); }); it("keeps plain Russian counterparty item-flow wording out of stale inventory context", async () => { executeAddressMcpQueryMock .mockResolvedValueOnce({ fetched_rows: 1, matched_rows: 1, raw_rows: [ { Counterparty: "Чепурнов П.Д.", Registrator: "Чепурнов П.Д." } ], rows: [], error: null }) .mockResolvedValueOnce({ fetched_rows: 1, matched_rows: 1, raw_rows: [ { Period: "2022-01-20T12:00:03Z", Registrator: "Поступление товаров и услуг 000000001 от 20.01.2022", AccountDt: "41.01", AccountKt: "60.01", Amount: 890660, Nomenclature: "Услуги по договору", Counterparty: "Чепурнов П.Д.", Contract: "Договор № 11/1 от 25.11.2020 г.", Organization: 'ООО "Альтернатива Плюс"' } ], rows: [], error: null }); const service = new AddressQueryService(); const result = await service.tryHandle("что нам отгружал чепурнов? какой товар или услугу?", { followupContext: { previous_intent: "inventory_on_hand_as_of_date", target_intent: "inventory_on_hand_as_of_date", previous_filters: { organization: 'ООО "Альтернатива Плюс"', as_of_date: "2026-04-24" }, previous_anchor_type: "organization", previous_anchor_value: 'ООО "Альтернатива Плюс"' } }); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); expect(result?.debug.selected_recipe).toBe("address_documents_by_counterparty_v1"); expect(String(result?.reply_text ?? "")).toContain("Чепурнов П.Д."); expect(String(result?.reply_text ?? "")).toContain("Услуги по договору"); }); it("routes account 60 tails wording to open items intent", () => { const result = resolveAddressIntent("хвосты покажи по счету 60 на август 2022"); expect(result.intent).toBe("open_items_by_counterparty_or_contract"); }); it("includes resolved full counterparty name in document reply", () => { const reply = composeFactualReply( "list_documents_by_counterparty", [ { period: "2022-08-15T00:00:00Z", registrator: "Поступление товаров и услуг 000000123 от 15.08.2022", account_dt: "41.01", account_kt: "60.01", amount: 12500, analytics: ["Чепурнов П.Д.", "Основной договор"], item: "Кабель силовой", organization: 'ООО "Альтернатива Плюс"' } ], { userMessage: "покажи все документы по чапурнову", counterpartyHint: "Чепурнов П.Д." } ); expect(reply.text).toContain("Контрагент: Чепурнов П.Д."); }); it("uses purchase document query for fuzzy counterparty item-flow wording", async () => { executeAddressMcpQueryMock.mockImplementation(async (request?: { query?: string }) => { const query = String(request?.query ?? ""); if (query.includes("Справочник.Контрагенты")) { return { fetched_rows: 1, matched_rows: 1, raw_rows: [ { Counterparty: "Чепурнов П.Д.", Registrator: "Чепурнов П.Д." } ], rows: [], error: null }; } return { fetched_rows: 2, matched_rows: 2, raw_rows: [ { Period: "2020-03-10T00:00:00Z", Registrator: "Поступление товаров и услуг 000000001 от 10.03.2020", AccountDt: "41.01", AccountKt: "60.01", Amount: 12000, Nomenclature: "Кабель силовой", Counterparty: "Чепурнов П.Д.", Contract: "Основной договор", Quantity: 2, Organization: 'ООО "Альтернатива Плюс"' }, { Period: "2020-03-14T00:00:00Z", Registrator: "Поступление товаров и услуг 000000002 от 14.03.2020", AccountDt: "41.01", AccountKt: "60.01", Amount: 5400, Nomenclature: "Патч-корд", Counterparty: "Чепурнов П.Д.", Contract: "Основной договор", Quantity: 10, Organization: 'ООО "Альтернатива Плюс"' } ], rows: [], error: null }; }); const service = new AddressQueryService(); const result = await service.tryHandle( "какие товары или услуги были отгружены нашей компании контрагентом чапурновым?" ); expect(result?.handled).toBe(true); expect(result?.response_type).toBe("FACTUAL_LIST"); expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); expect(result?.debug.address_coverage_evidence_v1?.coverage_status).toBe("full"); expect(result?.debug.address_coverage_evidence_v1?.evidence_basis).toBe("matched_rows"); expect(result?.debug.address_truth_gate_v1?.truth_gate_status).toBe("full_confirmed"); expect(String(result?.reply_text ?? "")).toContain("Контрагент: Чепурнов П.Д."); expect(String(result?.reply_text ?? "")).toContain("Позиции:"); expect(String(result?.reply_text ?? "")).toContain("Кабель силовой"); expect(String(result?.reply_text ?? "")).toContain("договор:"); expect(String(result?.reply_text ?? "")).toContain("дата:"); const query = String(executeAddressMcpQueryMock.mock.calls.at(-1)?.[0]?.query ?? ""); expect(query).toContain("Документ.ПоступлениеТоваровУслуг.Товары"); expect(query).toContain("Документ.ПоступлениеТоваровУслуг.Услуги"); expect(query).toContain("Товары.Ссылка.Контрагент.Наименование ПОДОБНО"); expect(query).not.toContain("контрагентом"); }); it("keeps resolved counterparty group name in user-facing document reply for svk wording", async () => { executeAddressMcpQueryMock .mockResolvedValueOnce({ fetched_rows: 1, matched_rows: 1, raw_rows: [ { Counterparty: "Группа СВК", Registrator: "Группа СВК" } ], rows: [], error: null }) .mockResolvedValueOnce({ fetched_rows: 1, matched_rows: 1, raw_rows: [ { Period: "2021-11-10T12:00:07Z", Registrator: "Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07", AccountDt: "0", AccountKt: "0", Amount: 20000, Counterparty: "Группа СВК", Contract: "Договор № 1-ПМ/2020 от 05.06.2020", Organization: 'ООО "Альтернатива Плюс"' } ], rows: [], error: null }); const service = new AddressQueryService(); const result = await service.tryHandle("покажи документы по свк"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); expect(String(result?.reply_text ?? "")).toContain("Контрагент: Группа СВК"); expect(String(result?.reply_text ?? "")).not.toContain("Контрагент: Группа Найдено"); }); it("prefers requested full counterparty label when document rows only expose a generic group label", () => { const reply = composeFactualReply( "list_documents_by_counterparty", [ { period: "2021-11-10T12:00:07Z", registrator: "Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07", account_dt: "0", account_kt: "0", amount: 20000, analytics: ["Группа", "Договор № 1-ПМ/2020 от 05.06.2020"], organization: 'ООО "Альтернатива Плюс"' } ], { userMessage: "а по свк", counterpartyHint: "Группа СВК" } ); expect(reply.text).toContain("Контрагент: Группа СВК. Найдено документов: 1."); expect(reply.text).not.toContain("Контрагент: Группа. Найдено документов"); }); it("keeps current resolved counterparty label over stale follow-up anchor during short retarget", async () => { executeAddressMcpQueryMock .mockResolvedValueOnce({ fetched_rows: 1, matched_rows: 1, raw_rows: [ { Counterparty: "Группа СВК", Registrator: "Группа СВК" } ], rows: [], error: null }) .mockResolvedValueOnce({ fetched_rows: 2, matched_rows: 2, raw_rows: [ { Period: "2021-11-10T12:00:07Z", Registrator: "Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07", AccountDt: "0", AccountKt: "0", Amount: 20000, Counterparty: "Группа", Contract: "Договор № 1-ПМ/2020 от 05.06.2020", Organization: 'ООО "Альтернатива Плюс"' }, { Period: "2021-09-29T12:00:03Z", Registrator: "Поступление на расчетный счет 00000000012 от 29.09.2021 12:00:03", AccountDt: "0", AccountKt: "0", Amount: 50000, Counterparty: "Группа", Contract: "Договор № 1-ПМ/2020 от 05.06.2020", Organization: 'ООО "Альтернатива Плюс"' } ], rows: [], error: null }); const service = new AddressQueryService(); const result = await service.tryHandle("а по свк", { followupContext: { previous_intent: "list_documents_by_counterparty", previous_filters: { counterparty: "Чепурнов П.Д." }, previous_anchor_type: "counterparty", previous_anchor_value: "Чепурнов П.Д." } }); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); expect(String(result?.reply_text ?? "")).toContain("Контрагент: Группа СВК"); expect(String(result?.reply_text ?? "")).not.toContain("Контрагент: Группа Найдено"); expect(String(result?.reply_text ?? "")).not.toContain("Контрагент: Чепурнов"); }); it("explains supplier payments and return when no supply rows are found", async () => { executeAddressMcpQueryMock.mockImplementation(async (request?: { query?: string }) => { const query = String(request?.query ?? ""); if (query.includes("Справочник.Контрагенты")) { return { fetched_rows: 1, matched_rows: 1, raw_rows: [ { Counterparty: "Чепурнов П.Д.", Registrator: "Чепурнов П.Д." } ], rows: [], error: null }; } if (query.includes("Документ.ПоступлениеТоваровУслуг.Товары") || query.includes("Документ.ПоступлениеТоваровУслуг.Услуги")) { return { fetched_rows: 0, matched_rows: 0, raw_rows: [], rows: [], error: null }; } return { fetched_rows: 3, matched_rows: 3, raw_rows: [ { Period: "2021-06-11T12:00:01Z", Registrator: "Списание с расчетного счета 00000000124 от 11.06.2021", AccountDt: "60.02", AccountKt: "51", Amount: 119210, SubcontoDt1: "Чепурнов П.Д.", SubcontoDt2: "Договор № 11/1 от 25.11.2020", SubcontoKt1: "ПАО СБЕРБАНК" }, { Period: "2021-05-18T12:00:00Z", Registrator: "Списание с расчетного счета 00000000112 от 18.05.2021", AccountDt: "60.02", AccountKt: "51", Amount: 180230, SubcontoDt1: "Чепурнов П.Д.", SubcontoDt2: "Договор № 11/1 от 25.11.2020", SubcontoKt1: "ПАО СБЕРБАНК" }, { Period: "2022-01-20T12:00:03Z", Registrator: "Поступление на расчетный счет 00000000001 от 20.01.2022", AccountDt: "51", AccountKt: "60.02", Amount: 299440, SubcontoKt1: "Чепурнов П.Д.", SubcontoKt2: "Договор № 11/1 от 25.11.2020", SubcontoDt1: "ПАО СБЕРБАНК" } ], rows: [], error: null }; }); const service = new AddressQueryService(); const result = await service.tryHandle( "какие товары или услуги были отгружены нашей компании контрагентом чапурновым?" ); expect(result?.handled).toBe(true); expect(String(result?.reply_text ?? "")).toContain("Подтвержденных поставок товаров или услуг не найдено"); expect(String(result?.reply_text ?? "")).toContain("исходящих оплат поставщику"); expect(String(result?.reply_text ?? "")).toContain("возвратов от поставщика"); expect(String(result?.reply_text ?? "")).toContain("Договор:"); expect(result?.debug.address_coverage_evidence_v1?.coverage_status).toBe("blocked"); expect(result?.debug.address_coverage_evidence_v1?.evidence_basis).toBe("unknown"); expect(result?.debug.reasons).toContain("counterparty_item_flow_no_supply_but_bank_activity_explained"); expect(executeAddressMcpQueryMock.mock.calls.length).toBeGreaterThanOrEqual(2); }); it("keeps account 60 tails in open-items route and mentions the account in reply", async () => { executeAddressMcpQueryMock.mockResolvedValueOnce({ fetched_rows: 1, matched_rows: 1, raw_rows: [ { Period: "2022-08-31T23:59:59Z", Registrator: "Поступление на расчетный счет 000000001 от 31.08.2022", AccountDt: "51", AccountKt: "60.01", Amount: 150000, SubcontoKt1: "ООО Поставщик", SubcontoKt2: "Договор поставки", Organization: 'ООО "Альтернатива Плюс"' } ], rows: [], error: null }); const service = new AddressQueryService(); const result = await service.tryHandle("хвосты покажи по счету 60 на август 2022"); expect(result?.handled).toBe(true); expect(result?.response_type).toBe("FACTUAL_LIST"); expect(result?.debug.detected_intent).toBe("open_items_by_counterparty_or_contract"); expect(String(result?.reply_text ?? "")).toContain("счету 60"); const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? ""); expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто"); expect(query).toContain("60%"); }); });