NODEDC_1C/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts

840 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import { detectAddressQuestionMode } from "../src/services/addressQueryClassifier";
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
import { classifyAddressQueryShape } from "../src/services/addressQueryShapeClassifier";
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
import { AddressQueryService } from "../src/services/addressQueryService";
import { buildAddressRecipePlan, selectAddressRecipe } from "../src/services/addressRecipeCatalog";
import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage";
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
describe("address query shape classifier", () => {
it("classifies explain question as deep-shape", () => {
const result = classifyAddressQueryShape("Why VAT chain does not match?");
expect(result.shape).toBe("EXPLAIN_OR_REASON");
expect(result.confidence).toBe("high");
});
it("classifies aggregate lookup question", () => {
const result = classifyAddressQueryShape("who owes us today?");
expect(result.shape).toBe("AGGREGATE_LOOKUP");
});
it("classifies compound factual question", () => {
const result = classifyAddressQueryShape("who owes us and who we owe today?");
expect(result.shape).toBe("COMPOUND_FACTUAL_QUERY");
});
it("keeps company lookup phrasing in address lane", () => {
const result = detectAddressQuestionMode("какие компании есть в базе");
expect(result.mode).toBe("address_query");
});
it("keeps loose by-anchor follow-up phrasing in address lane", () => {
const result = detectAddressQuestionMode("за любой период есть что-то по свк?");
expect(result.mode).toBe("address_query");
});
it("keeps slang transaction phrasing in address lane", () => {
const result = detectAddressQuestionMode("транзакции по свк за 2020");
expect(result.mode).toBe("address_query");
});
it("keeps short balance slang with compact account token in address lane", () => {
const result = detectAddressQuestionMode("скока по 60.02 на конец 2020-12");
expect(result.mode).toBe("address_query");
});
});
describe("address compose stage utf8 headers", () => {
it("renders readable russian header for contract document list", () => {
const reply = composeFactualReply("list_documents_by_contract", [
{
period: "2020-10-15T13:34:53Z",
registrator: "Списание с расчетного счета 00000000246",
account_dt: "66.02",
account_kt: "51",
amount: 30819.47,
analytics: []
}
]);
expect(reply.text).toContain("Собран список документов по договору (live address lane).");
});
it("renders readable russian header for contract bank operations", () => {
const reply = composeFactualReply("bank_operations_by_contract", [
{
period: "2020-10-15T13:34:53Z",
registrator: "Списание с расчетного счета 00000000246",
account_dt: "66.02",
account_kt: "51",
amount: 30819.47,
analytics: []
}
]);
expect(reply.text).toContain("Собран список банковских операций по договору (live address lane).");
});
});
describe("address intent resolver expansion (M2.3a)", () => {
it("resolves documents by counterparty intent", () => {
const result = resolveAddressIntent("show documents by counterparty Alfa from 2020-07-01 to 2020-07-31");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves bank operations by counterparty intent", () => {
const result = resolveAddressIntent("show bank operations by counterparty Alfa");
expect(result.intent).toBe("bank_operations_by_counterparty");
});
it("resolves documents forming balance intent", () => {
const result = resolveAddressIntent("which documents form balance for account 62 as of 2020-07-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves documents forming balance for russian participle phrasing", () => {
const result = resolveAddressIntent("Показать документы, формирующие остаток по счету 60.01 на дату 2020-07-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves documents forming balance for slang phrase with compact account token", () => {
const result = resolveAddressIntent("раскрой остаток 60.01 по документам на конец июля 2020");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves documents forming balance for 'доки под остатком' slang phrase", () => {
const result = resolveAddressIntent("доки под остатком 60.01 на 2020-07-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves documents by company phrase as counterparty intent", () => {
const result = resolveAddressIntent("Какие документы доступны по компании СВК за 2021 год?");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves bank operations by supplier phrase", () => {
const result = resolveAddressIntent("Покажи платежи по поставщику Альфа за июль 2020");
expect(result.intent).toBe("bank_operations_by_counterparty");
});
it("resolves documents by contract intent", () => {
const result = resolveAddressIntent("Покажи документы по договору 15/24 за 2020");
expect(result.intent).toBe("list_documents_by_contract");
});
it("resolves bank operations by contract intent", () => {
const result = resolveAddressIntent("Покажи банковские операции по договору 15/24");
expect(result.intent).toBe("bank_operations_by_contract");
});
it("resolves shorthand bank-by-contract slang intent", () => {
const result = resolveAddressIntent("покажи банк опер по дог 15/24 пж");
expect(result.intent).toBe("bank_operations_by_contract");
});
it("resolves debt-by-contract query to open items intent", () => {
const result = resolveAddressIntent("Есть ли долг по договору 15/24 на 2020-07-31");
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
});
it("resolves unclosed contracts list query without specific anchor", () => {
const result = resolveAddressIntent("Покажи незакрытые договоры на 2020-12-31");
expect(result.intent).toBe("list_open_contracts");
});
it("resolves bank operations by contract for normalized phrase with linked contract wording", () => {
const result = resolveAddressIntent(
"Показать банковские операции (счета 51, 60, 62) связанные с договором 15/24."
);
expect(result.intent).toBe("bank_operations_by_contract");
});
it("keeps bank_operations_by_counterparty even when account hints are present", () => {
const result = resolveAddressIntent("Показать банковские операции (счета 51, 62) для контрагента СВК за 2020 год");
expect(result.intent).toBe("bank_operations_by_counterparty");
});
it("resolves documents by client phrase", () => {
const result = resolveAddressIntent("Выведи документы по клиенту Бета за 2020-07");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves short slang docs phrase with loose by-anchor", () => {
const result = resolveAddressIntent("какие доки есть по свк за 2021");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves typo slang docs phrase with implicit anchor", () => {
const result = resolveAddressIntent("свк доки за 20год покеж");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves noisy docs phrase with slang tail", () => {
const result = resolveAddressIntent("свк 20 год - покажи доки плс");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves slang transactions phrase by counterparty", () => {
const result = resolveAddressIntent("транзакции по свк за 2020");
expect(result.intent).toBe("bank_operations_by_counterparty");
});
it("resolves short balance slang with compact account token", () => {
const result = resolveAddressIntent("скока по 60.02 на конец 2020-12");
expect(result.intent).toBe("account_balance_snapshot");
});
it("resolves colloquial 'что на счете' phrasing as account balance snapshot", () => {
const result = resolveAddressIntent("что на счете 60 на 2020.05");
expect(result.intent).toBe("account_balance_snapshot");
});
it("resolves mixed ru/en balance phrasing with account token", () => {
const result = resolveAddressIntent("баланс account 60.01 as of 2020-07-31");
expect(result.intent).toBe("account_balance_snapshot");
});
it("resolves 'по докам' slang as documents forming balance", () => {
const result = resolveAddressIntent("раскидай остаток 62.01 по докам на 2020-12-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves english compact docs-forming phrasing", () => {
const result = resolveAddressIntent("docs forming balance 60.01 as of 2020-07-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves loose by-anchor follow-up as documents by counterparty fallback", () => {
const result = resolveAddressIntent("за любой период есть что-то по свк?");
expect(result.intent).toBe("list_documents_by_counterparty");
});
});
describe("address filter extraction for balance drilldown", () => {
it("defaults as_of_date for documents_forming_balance when date is omitted", () => {
const result = extractAddressFilters("which documents form balance for account 62", "documents_forming_balance");
expect(result.extracted_filters.account).toBe("62");
expect(result.extracted_filters.as_of_date).toBeDefined();
expect(result.missing_required_filters).toEqual([]);
});
it("cuts period tail from counterparty anchor", () => {
const result = extractAddressFilters(
"Покажи документы по контрагенту test_cp с 2020-07-01 по 2020-07-31",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("test_cp");
expect(result.extracted_filters.period_from).toBe("2020-07-01");
expect(result.extracted_filters.period_to).toBe("2020-07-31");
});
it("cuts all-time tail from counterparty anchor and skips 90-day default window", () => {
const result = extractAddressFilters(
"Покажи документы по контрагенту тестовый за все время",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("тестовый");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("keeps all-time period by default for counterparty docs query without explicit window", () => {
const result = extractAddressFilters(
"Покажи документы по контрагенту тестовый",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("тестовый");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("extracts counterparty from company phrase and derives year period", () => {
const result = extractAddressFilters(
"Какие документы доступны по компании СВК за 2021 год?",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.period_from).toBe("2021-01-01");
expect(result.extracted_filters.period_to).toBe("2021-12-31");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("extracts counterparty from supplier phrase and derives month period", () => {
const result = extractAddressFilters(
"Покажи документы по поставщику Альфа за июль 2020",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("Альфа");
expect(result.extracted_filters.period_from).toBe("2020-07-01");
expect(result.extracted_filters.period_to).toBe("2020-07-31");
expect(result.warnings).toContain("period_derived_from_month_phrase");
});
it("cuts period-end tail from counterparty anchor and keeps as_of for open-items query", () => {
const result = extractAddressFilters(
"Покажи хвосты по контрагенту СВК на конец периода 2020-12-31",
"open_items_by_counterparty_or_contract"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.as_of_date).toBe("2020-12-31");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).toContain("period_window_cleared_for_as_of_intent");
});
it("derives month period for balance snapshot from 'на май 2020'", () => {
const result = extractAddressFilters("Какой остаток по счету 60 на май 2020", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60");
expect(result.extracted_filters.period_from).toBe("2020-05-01");
expect(result.extracted_filters.period_to).toBe("2020-05-31");
expect(result.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result.warnings).toContain("period_derived_from_month_phrase");
expect(result.warnings).toContain("as_of_date_derived_from_period_to");
});
it("derives month period for balance snapshot from 'на 2020.05'", () => {
const result = extractAddressFilters("Какой остаток по счету 60 на 2020.05", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60");
expect(result.extracted_filters.period_from).toBe("2020-05-01");
expect(result.extracted_filters.period_to).toBe("2020-05-31");
expect(result.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result.warnings).toContain("period_derived_from_month_phrase");
expect(result.warnings).toContain("as_of_date_derived_from_period_to");
});
it("derives month period for balance snapshot from 'на 2020 май'", () => {
const result = extractAddressFilters("Какой остаток по счету 60 на 2020 май", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60");
expect(result.extracted_filters.period_from).toBe("2020-05-01");
expect(result.extracted_filters.period_to).toBe("2020-05-31");
expect(result.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result.warnings).toContain("period_derived_from_month_phrase");
expect(result.warnings).toContain("as_of_date_derived_from_period_to");
});
it("extracts dotted account by heuristic for docs-forming phrasing without 'счет' keyword", () => {
const result = extractAddressFilters(
"раскрой остаток 60.01 по документам на конец июля 2020",
"documents_forming_balance"
);
expect(result.extracted_filters.account).toBe("60.01");
expect(result.extracted_filters.as_of_date).toBe("2020-07-31");
expect(result.warnings).toContain("account_anchor_derived_from_heuristic_token");
});
it("extracts dotted account by heuristic for short balance slang", () => {
const result = extractAddressFilters("скока по 60.02 на конец 2020-12", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60.02");
expect(result.extracted_filters.as_of_date).toBe("2020-12-31");
expect(result.warnings).toContain("account_anchor_derived_from_heuristic_token");
});
it("drops accidental account for non-account intent without explicit account cue", () => {
const result = extractAddressFilters("покажи банк операции по свк за 2020", "bank_operations_by_counterparty");
expect(result.extracted_filters.account).toBeUndefined();
});
it("extracts leading counterparty token for short bank phrase", () => {
const result = extractAddressFilters("свк списания/поступления за 2020", "bank_operations_by_counterparty");
expect(result.extracted_filters.counterparty).toBe("свк");
expect(
result.warnings.includes("counterparty_anchor_derived_from_leading_token") ||
result.warnings.includes("counterparty_anchor_derived_from_free_text_heuristic")
).toBe(true);
});
it("treats 'за весь период' as all-time hint and does not force 90-day default", () => {
const result = extractAddressFilters(
"Покажи банковские операции по клиенту Бета за весь период",
"bank_operations_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("Бета");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("extracts loose by-anchor and year period for short slang docs phrase", () => {
const result = extractAddressFilters(
"какие доки есть по свк за 2021",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2021-01-01");
expect(result.extracted_filters.period_to).toBe("2021-12-31");
expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("extracts implicit counterparty and short-year period for typo slang docs phrase", () => {
const result = extractAddressFilters(
"свк доки за 20год покеж",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("counterparty_anchor_derived_from_implicit_phrase");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("extracts free-text counterparty and relaxed short-year period from noisy phrase", () => {
const result = extractAddressFilters(
"свк 20 год - покажи доки плс",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("counterparty_anchor_derived_from_free_text_heuristic");
expect(result.warnings).toContain("period_derived_from_year_phrase");
expect(result.extracted_filters.counterparty).not.toBe("плс");
});
it("extracts short ordinal year period from noisy docs phrase", () => {
const result = extractAddressFilters(
"бля епт покажи доки по свк за 20-й",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("does not use action verb as counterparty when phrase is 'Показать документы <counterparty>'", () => {
const result = extractAddressFilters(
"Показать документы СВК за 2020 год.",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.counterparty).not.toBe("Показать");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("extracts counterparty and short year from transliterated noisy phrase", () => {
const result = extractAddressFilters(
"svk doki za 20 god pokezh",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("svk");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(
result.warnings.some(
(warning) =>
warning === "counterparty_anchor_derived_from_free_text_heuristic" ||
warning === "counterparty_anchor_derived_from_implicit_phrase"
)
).toBe(true);
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("repairs mojibake phrase before extracting counterparty filters", () => {
const result = extractAddressFilters(
"Показать документы СВК за 2020 год.",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("extracts explicit year range period from phrase", () => {
const result = extractAddressFilters(
"Какие документы по СВК за 2000 - 2025 год?",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.period_from).toBe("2000-01-01");
expect(result.extracted_filters.period_to).toBe("2025-12-31");
expect(result.warnings).toContain("period_derived_from_year_range_phrase");
});
it("extracts contract and year period for contract document list", () => {
const result = extractAddressFilters(
"Покажи документы по договору 15/24 за 2020 год",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("15/24");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("cuts trailing as-of date from contract anchor", () => {
const result = extractAddressFilters(
"Покажи документы по договору 1-ПМ/2020 на дату 31.07.2020",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("1-ПМ/2020");
expect(result.extracted_filters.period_from).toBe("2020-07-01");
expect(result.extracted_filters.period_to).toBe("2020-07-31");
});
it("does not force 90-day default window for by-contract query without explicit period", () => {
const result = extractAddressFilters(
"Покажи документы по договору 15/24",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("15/24");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("extracts heuristic contract token for noisy contract phrase", () => {
const result = extractAddressFilters(
"доки 15/24 за 2020",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("15/24");
expect(result.warnings).toContain("contract_anchor_derived_from_heuristic_token");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("trims english year tail from contract anchor", () => {
const result = extractAddressFilters(
"docs by contract 15/24 year 2020",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("15/24");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("trims trailing separated year from contract anchor", () => {
const result = extractAddressFilters(
"docs by contract 15/24 2020",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("15/24");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("extracts multiline year range period from phrase", () => {
const result = extractAddressFilters(
"Какие документы по СВК за 2000 - 2025\n год?",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.period_from).toBe("2000-01-01");
expect(result.extracted_filters.period_to).toBe("2025-12-31");
expect(result.warnings).toContain("period_derived_from_year_range_phrase");
expect(result.warnings).not.toContain("period_derived_from_year_phrase");
});
it("extracts russian year range period from 'с ... по ...' phrase", () => {
const result = extractAddressFilters(
"какие есть доки по свк с 2020 по 2025 год",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2025-12-31");
expect(result.warnings).toContain("period_derived_from_year_range_phrase");
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("treats 'за любой период' as all-time hint and keeps loose by-anchor", () => {
const result = extractAddressFilters(
"за любой период есть что-то по свк?",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase");
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
});
describe("address query limited taxonomy and stage diagnostics", () => {
it("returns missing_anchor for open items without concrete counterparty/contract anchor", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("show open items by contract");
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.limited_reason_category).toBe("missing_anchor");
expect(result?.debug.mcp_call_status).toBe("skipped");
});
it("does not return fallback factual rows for unmatched open-items contract anchor", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Покажи открытые позиции по договору 15/24");
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.reply_type).toBe("partial_coverage");
expect(result?.debug.detected_intent).toBe("open_items_by_counterparty_or_contract");
expect(result?.debug.rows_matched).toBe(0);
expect(["empty_match", "missing_anchor"]).toContain(result?.debug.limited_reason_category);
expect(String(result?.assistant_reply ?? "")).not.toContain("Собраны открытые позиции");
});
it("routes contract document list intent into address recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("show documents by contract 15/24");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_documents_by_contract");
expect(result?.debug.selected_recipe).toBe("address_documents_by_contract_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("routes bank operations by contract intent into address recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Покажи банковские операции по договору 15/24");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("bank_operations_by_contract");
expect(result?.debug.selected_recipe).toBe("address_bank_operations_by_contract_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("includes resolver and row-stage diagnostics", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("which documents form balance for account 62 as of 2020-07-31");
expect(result?.handled).toBe(true);
expect(["LIMITED_WITH_REASON", "FACTUAL_LIST"]).toContain(result?.response_type);
expect(result?.debug.anchor_type).toBe("account");
expect(result?.debug.rows_fetched).toBeTypeOf("number");
expect(result?.debug.raw_rows_received).toBeTypeOf("number");
expect(result?.debug.rows_after_account_scope).toBeTypeOf("number");
expect(result?.debug.rows_materialized).toBeTypeOf("number");
expect(result?.debug.rows_after_recipe_filter).toBeTypeOf("number");
expect(result?.debug.rows_matched).toBeTypeOf("number");
expect(["strict", "preferred"]).toContain(result?.debug.account_scope_mode);
expect(result?.debug.account_scope_fallback_applied).toBeTypeOf("boolean");
expect(result?.debug.mcp_call_status_legacy).toBeDefined();
expect(result?.debug.match_failure_stage).toBeDefined();
expect([
"error",
"no_raw_rows",
"raw_rows_received_but_not_materialized",
"materialized_but_not_anchor_matched",
"materialized_but_filtered_out_by_recipe",
"materialized_but_not_matched",
"matched_non_empty"
]).toContain(result?.debug.mcp_call_status);
expect(result?.debug.raw_row_keys_sample).toBeDefined();
expect(result?.debug.materialization_drop_reason).toBeDefined();
expect(result?.debug.account_scope_fields_checked).toBeDefined();
expect(result?.debug.account_scope_match_strategy).toBe("account_code_regex_plus_alias_map_v1");
expect(result?.debug.account_scope_drop_reason).toBeDefined();
});
it("keeps short slang docs request in address lane (no deep fallback)", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какие доки есть по свк за 2021");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_mode).toBe("address_query");
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.extracted_filters.counterparty).toBe("свк");
expect(result?.debug.extracted_filters.period_from).toBe("2021-01-01");
expect(result?.debug.extracted_filters.period_to).toBe("2021-12-31");
});
it("keeps typo slang docs request in address lane and extracts implicit anchor", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("свк доки за 20год покеж");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_mode).toBe("address_query");
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.extracted_filters.counterparty).toBe("свк");
expect(result?.debug.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.debug.extracted_filters.period_to).toBe("2020-12-31");
});
it("keeps noisy docs request in address lane and ignores slang tail token", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("свк 20 год - покажи доки плс");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_mode).toBe("address_query");
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.extracted_filters.counterparty).toBe("свк");
expect(result?.debug.extracted_filters.counterparty).not.toBe("плс");
expect(result?.debug.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.debug.extracted_filters.period_to).toBe("2020-12-31");
});
it("auto-broadens out-of-window period and returns available factual rows", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие документы по СВК за 2000 год?");
expect(result?.handled).toBe(true);
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
if (result?.response_type === "FACTUAL_LIST") {
expect(result?.debug.limited_reason_category).toBeNull();
}
});
});
describe("address decompose stage follow-up carryover", () => {
it("keeps short period follow-up in address lane and preserves previous counterparty anchor", () => {
const result = runAddressDecomposeStage("а теперь только за май 2020", {
previous_intent: "list_documents_by_counterparty",
previous_filters: {
counterparty: "свк",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
expect(result?.filters.extracted_filters.counterparty).toBe("свк");
expect(result?.filters.extracted_filters.period_from).toBe("2020-05-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("inherits as_of_date from previous period for same-date balance follow-up", () => {
const result = runAddressDecomposeStage("а по счету 60.01 на ту же дату", {
previous_intent: "list_documents_by_counterparty",
previous_filters: {
counterparty: "свк",
period_from: "2020-05-01",
period_to: "2020-05-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("account_balance_snapshot");
expect(result?.filters.extracted_filters.account).toBe("60.01");
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("does not downgrade inherited follow-up anchor to missing_anchor when period has no rows", async () => {
const service = new AddressQueryService();
const seed = await service.tryHandle("покажи документы по свк за 2020");
expect(seed?.handled).toBe(true);
const followup = await service.tryHandle("а теперь только за май 2020", {
followupContext: {
previous_intent: (seed?.debug.detected_intent as any) ?? "list_documents_by_counterparty",
previous_filters: seed?.debug.extracted_filters,
previous_anchor_type: (seed?.debug.anchor_type as any) ?? "counterparty",
previous_anchor_value: seed?.debug.anchor_value_resolved ?? seed?.debug.anchor_value_raw ?? null
}
});
expect(followup?.handled).toBe(true);
if (followup?.reply_type === "partial_coverage") {
expect(followup?.debug.limited_reason_category).not.toBe("missing_anchor");
}
});
});
describe("address recipe catalog counterparty filtering", () => {
it("boosts limit for all-time counterparty queries", () => {
const filters = extractAddressFilters(
"Покажи документы по контрагенту тестовый за все время",
"list_documents_by_counterparty"
).extracted_filters;
const selected = selectAddressRecipe("list_documents_by_counterparty", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.limit).toBe(1000);
});
it("boosts limit for english all-time counterparty queries", () => {
const filters = extractAddressFilters(
"show documents by counterparty test_cp for all time",
"list_documents_by_counterparty"
).extracted_filters;
const selected = selectAddressRecipe("list_documents_by_counterparty", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.limit).toBe(1000);
});
it("cuts english all-time tail from counterparty anchor", () => {
const result = extractAddressFilters(
"show documents by counterparty test_cp for all time",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("test_cp");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("boosts limit for account snapshot queries with explicit account", () => {
const filters = extractAddressFilters(
"Какой остаток по счету 60 на дату 2020-07-31",
"account_balance_snapshot"
).extracted_filters;
const selected = selectAddressRecipe("account_balance_snapshot", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.limit).toBe(200);
});
it("allows extended limit for open-items by contract intent", () => {
const selected = selectAddressRecipe("open_items_by_counterparty_or_contract", {
contract: "15/24",
limit: 1000
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
contract: "15/24",
limit: 1000
});
expect(plan.limit).toBe(1000);
});
it("allows extended limit for open-contracts intent", () => {
const selected = selectAddressRecipe("list_open_contracts", {
as_of_date: "2020-12-31",
limit: 1000
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
as_of_date: "2020-12-31",
limit: 1000
});
expect(plan.limit).toBe(1000);
});
it("injects account condition into movements query for account snapshot", () => {
const filters = extractAddressFilters(
"Какой остаток по счету 60 на дату 2020-07-31",
"account_balance_snapshot"
).extracted_filters;
const selected = selectAddressRecipe("account_balance_snapshot", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.query).toContain("Движения.СчетДт.Код");
expect(plan.query).toContain("ПОДОБНО \"60%\"");
});
it("injects subaccount condition variants into movements query for documents_forming_balance", () => {
const filters = extractAddressFilters(
"Какие документы формируют остаток по счету 60.01 на дату 2020-07-31",
"documents_forming_balance"
).extracted_filters;
const selected = selectAddressRecipe("documents_forming_balance", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.query).toContain("ПОДОБНО \"60.01%\"");
expect(plan.query).toContain("ПОДОБНО \"60.1%\"");
});
});