840 lines
40 KiB
TypeScript
840 lines
40 KiB
TypeScript
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%\"");
|
||
});
|
||
});
|