NODEDC_1C/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswer...

1428 lines
66 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, 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<Record<string, unknown>>, 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<Record<string, unknown>>; 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 buildCustomQueryDeps(result: {
fetched_rows: number;
matched_rows: number;
rows: Array<Record<string, unknown>>;
raw_rows?: Array<Record<string, unknown>>;
error?: string | null;
}) {
return {
executeAddressMcpQuery: vi.fn(async () => ({
fetched_rows: result.fetched_rows,
matched_rows: result.matched_rows,
rows: result.rows,
raw_rows: result.raw_rows ?? result.rows,
error: result.error ?? null
}))
};
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, 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.confirmed_lines[0]).toContain("matched_rows=1");
expect(draft.inference_lines[0]).toContain("меньше месяца");
expect(draft.inference_lines.join("\n")).toContain("Первая найденная активность: 2020-01-15");
expect(draft.inference_lines.join("\n")).toContain("не юридически подтвержденный возраст регистрации");
expect(pilot.evidence.inferred_facts).toContain(
"Activity window is bounded by first=2020-01-15, latest=2020-01-15, matched_rows=1"
);
expect(pilot.evidence.inferred_facts).toContain("Activity-window inference is not legal registration age");
expect(draft.unknown_lines).toContain("Legal registration date is not proven by this MCP discovery pilot");
expect(draft.unknown_lines).toContain(
"Business activity before the first confirmed 1C activity row 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("Доступ к 1С во время проверки оборвался; подтвержденные строки не получены.");
expect(draft.limitation_lines).not.toContain("MCP fetch failed: timeout");
expect(draft.next_step_line).toContain("доступа к 1С");
expect(draft.next_step_line).not.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.headline).toContain("2020");
expect(draft.headline).toContain("SVK");
expect(draft.confirmed_lines).toContain("В 1С найдены строки документов по контрагенту SVK за 2020.");
expect(draft.inference_lines).toContain(
"Срез документов по контрагенту SVK за 2020 ограничен только подтвержденными строками документов, найденными этим поиском."
);
expect(draft.unknown_lines).toContain(
"Полный исторический срез документов по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
);
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.headline).toContain("2020");
expect(draft.headline).toContain("SVK");
expect(draft.confirmed_lines).toContain("В 1С найдены строки движений по контрагенту SVK за 2020.");
expect(draft.inference_lines).toContain(
"Срез движений по контрагенту SVK за 2020 ограничен только подтвержденными строками движений, найденными этим поиском."
);
expect(draft.unknown_lines).toContain(
"Полный исторический срез движений по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
);
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("turns business overview multi-probe evidence into an analyst-safe draft", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "all_time_scope",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" },
{ Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Клиент Б" }
]
},
{
rows: [{ Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Поставщик А" }]
},
{
rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("бизнес-обзор");
expect(draft.confirmed_lines.join("\n")).toContain("Входящие поступления");
expect(draft.confirmed_lines.join("\n")).toContain("Самый крупный подтвержденный клиент");
expect(draft.inference_lines.join("\n")).toContain("Аналитический вывод по оборотам");
expect(draft.inference_lines.join("\n")).toContain("Концентрация входящего потока");
expect(draft.inference_lines.join("\n")).toContain("Сводный LLM-аудит");
expect(draft.inference_lines.join("\n")).toContain("не прибыль и не маржа");
expect(draft.unknown_lines.join("\n")).toContain("Прибыль и маржа");
expect(draft.unknown_lines.join("\n")).toContain("Налоговая/VAT-позиция");
expect(draft.must_not_claim).toContain("Do not present business overview cash-flow spread as profit or margin.");
expect(draft.reason_codes).toContain("answer_contains_business_overview");
expect(draft.reason_codes).toContain("answer_contains_business_overview_analyst_synthesis");
});
it("surfaces checked VAT/tax position in business overview without treating it as profit", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{
rows: [
{ Регистратор: "VAT_BOOK_SALES", СчетДт: "68.02", Сумма: 40000 },
{ Регистратор: "VAT_BOOK_PURCHASES", СчетДт: "19", Сумма: 12000 }
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
]
},
{
rows: [
{ Period: "2020-03-01T00:00:00", Amount: 200000, Item: "Товар А", Counterparty: "Клиент А", СчетКт: "41.01" },
{ Period: "2020-02-01T00:00:00", Amount: 120000, Item: "Товар А", Counterparty: "Поставщик А", СчетДт: "41.01" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain("НДС-позиция");
expect(draft.headline).toContain("торговый margin proxy");
expect(draft.confirmed_lines.join("\n")).toContain("НДС-позиция за 2020");
expect(draft.confirmed_lines.join("\n")).toContain("нетто к уплате 28 000 руб.");
expect(draft.confirmed_lines.join("\n")).toContain("Торговый margin proxy за 2020");
expect(draft.confirmed_lines.join("\n")).toContain("валовый спред proxy 80 000 руб.");
expect(draft.confirmed_lines.join("\n")).toContain("не чистая прибыль");
expect(draft.inference_lines.join("\n")).toContain("торговый спред proxy 80 000 руб.");
expect(draft.inference_lines.join("\n")).toContain("не прибыль и не маржа");
expect(draft.unknown_lines.join("\n")).toContain("Чистая прибыль");
expect(draft.unknown_lines.join("\n")).not.toContain("Налоговая/VAT-позиция");
expect(draft.reason_codes).toContain("answer_contains_business_overview_tax_position");
expect(draft.reason_codes).toContain("answer_contains_business_overview_trading_margin_proxy");
expect(draft.must_not_claim).toContain("Do not present business overview cash-flow spread as profit or margin.");
expect(draft.must_not_claim).toContain("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin.");
});
it("surfaces checked debt-position and open-settlement quality without treating them as overdue debt", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" }
]
},
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 40000, Counterparty: "Поставщик А" }
]
},
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А от 10.02.2019" },
{ Period: "2020-12-31T00:00:00", Amount: 50000, Counterparty: "Поставщик А", Contract: "Договор Б от 15.03.2020" }
]
},
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" },
{ Period: "2020-12-15T00:00:00", Registrator: "Поступление 2" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain("долговой срез");
expect(draft.headline).toContain("качество открытых расчетов");
expect(draft.headline).toContain("возрастной сигнал открытых расчетов");
expect(draft.confirmed_lines.join("\n")).toContain("Долговой срез на 2020-12-31");
expect(draft.confirmed_lines.join("\n")).toContain("Качество открытых расчетов на 2020-12-31");
expect(draft.confirmed_lines.join("\n")).toContain("Возрастной сигнал открытых расчетов");
expect(draft.confirmed_lines.join("\n")).toContain("не due-date анализ");
expect(draft.confirmed_lines.join("\n")).toContain("нетто");
expect(draft.inference_lines.join("\n")).toContain("Риски и контуры внимания");
expect(draft.inference_lines.join("\n")).toContain("самый старый договорный возрастной сигнал");
expect(draft.inference_lines.join("\n")).toContain("Сводный LLM-аудит");
expect(draft.unknown_lines.join("\n")).toContain("due-date");
expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_position");
expect(draft.reason_codes).toContain("answer_contains_business_overview_open_settlement_quality");
expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_age_signal");
expect(draft.reason_codes).toContain("answer_contains_business_overview_analyst_synthesis");
expect(draft.must_not_claim).toContain("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
expect(draft.must_not_claim).toContain("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
});
it("surfaces checked inventory-position snapshot in business overview without treating it as warehouse liquidity", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Тест",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
{ Period: "2020-12-31T00:00:00", Amount: 50000, Quantity: 5, Item: "Товар Б" }
]
},
{
rows: [
{ Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Товар А" }
]
},
{ rows: [{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" }] },
{
rows: [
{ Period: "2020-03-01T00:00:00", Amount: 600000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
{ Period: "2020-02-01T00:00:00", Amount: 240000, Item: "Товар А", Counterparty: "Поставщик А", AccountDt: "41.01" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain("складской срез");
expect(draft.headline).toContain("оборотный proxy склада");
expect(draft.headline).toContain("staleness risk proxy склада");
expect(draft.headline).toContain("резервы/списания/ликвидационная стоимость склада");
expect(draft.headline).not.toContain("полноценная складская ликвидность");
expect(draft.confirmed_lines.join("\n")).toContain("Складской срез на 2020-12-31");
expect(draft.confirmed_lines.join("\n")).toContain("Товар А");
expect(draft.confirmed_lines.join("\n")).toContain("Оборотный proxy склада за 2020");
expect(draft.confirmed_lines.join("\n")).toContain("sales-to-stock ratio 2x");
expect(draft.confirmed_lines.join("\n")).toContain("Staleness risk proxy склада");
expect(draft.confirmed_lines.join("\n")).toContain("зона наблюдения");
expect(draft.inference_lines.join("\n")).toContain("оборотный proxy склада");
expect(draft.inference_lines.join("\n")).toContain("staleness risk proxy склада");
expect(draft.unknown_lines.join("\n")).toContain("Резервы");
expect(draft.next_step_line).toContain("НДС/налоговую позицию за явный период");
expect(draft.next_step_line).toContain("качество открытых расчетов");
expect(draft.next_step_line).toContain("резервы, списания, неликвидность и ликвидационную стоимость склада");
expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_position");
expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_turnover_proxy");
expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_staleness_risk_proxy");
expect(draft.must_not_claim).toContain("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
expect(draft.must_not_claim).toContain("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
});
it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "all_time_scope",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_all_time_scope_hint"]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: [],
explicit_organization_scope: "ООО Альтернатива Плюс",
unsupported_but_understood_family: "movement_evidence"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildCustomQueryDeps({
fetched_rows: 100,
matched_rows: 0,
rows: [],
raw_rows: [{ Period: "2020-06-30T00:00:00", Organization: "ООО Альтернатива Плюс", Registrar: "Move1" }]
})
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("bounded_inference_only");
expect(draft.headline).toContain("движени");
expect(draft.headline).toContain("все доступное время");
expect(draft.headline).not.toContain("за 2020");
expect(draft.inference_lines.join("\n")).not.toContain("за 2020");
});
it("keeps bounded-only movement answers tied to the resolved entity and checked period", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildCustomQueryDeps({
fetched_rows: 100,
matched_rows: 0,
rows: [],
raw_rows: [{ Period: "2020-06-30T00:00:00", Counterparty: "Группа СВК", Registrar: "Move1" }]
})
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("bounded_inference_only");
expect(draft.headline).toContain("движени");
expect(draft.headline).toContain("Группа СВК");
expect(draft.headline).toContain("2020");
expect(draft.inference_lines).toContain(
"По движениям по контрагенту Группа СВК за 2020 удалось проверить только ограниченный срез 1С; подтвержденных строк движений этим поиском не найдено."
);
expect(draft.unknown_lines).toContain(
"Полный исторический срез движений по контрагенту Группа СВК вне периода 2020 этим поиском не подтвержден."
);
});
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 organization rather than counterparty when a ranked value-flow ask already has the period", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("рейтинг");
expect(draft.next_step_line).toContain("организацию");
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
it("asks for both organization and period when an open ranking still misses both axes", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "period_required",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "clarification_required",
clarification_gaps: ["organization", "period"],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_ranking_top_desc",
"data_need_graph_open_scope_total_needs_organization",
"data_need_graph_has_clarification_gaps"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
seeded_ranking_need: "top_desc"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("период");
expect(draft.headline).toContain("организац");
expect(draft.next_step_line).toContain("период");
expect(draft.next_step_line).toContain("организац");
});
it("renders confirmed ranked value-flow without raw technical evidence lines", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{
Period: "2020-01-15T00:00:00",
Amount: 12000,
Counterparty: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a, \u041f\u0410\u041e",
Organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
},
{
Period: "2020-02-20T00:00:00",
Amount: 5000,
Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a",
Organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const userText = [...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines].join("\n");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.confirmed_lines).toHaveLength(1);
expect(userText).toContain("\u0411\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0451\u0441 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
expect(userText).toContain("\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441");
expect(userText).not.toContain("1C incoming value-flow");
expect(userText).not.toContain("Full ranking outside");
expect(draft.unknown_lines[0]).toContain("\u041f\u043e\u043b\u043d\u044b\u0439 \u0440\u0435\u0439\u0442\u0438\u043d\u0433");
});
it("does not overclaim a comparative ranking when only one counterparty is present", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2021",
seeded_ranking_need: "top_desc"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{
Period: "2021-01-15T00:00:00",
Amount: 8560025,
Counterparty: "Группа СВК",
Organization: "ООО Альтернатива Плюс"
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const text = draft.confirmed_lines.join("\n");
expect(text).toContain("найден один контрагент");
expect(text).toContain("Группа СВК");
expect(text).toContain("не полноценный сравнительный рейтинг");
expect(text).not.toContain("Больше всего денег принёс");
});
it("asks for both organization and period when an open total still misses both axes", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "period_required",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization", "period"],
decomposition_candidates: ["collect_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_open_scope_total_without_subject",
"data_need_graph_open_scope_total_needs_organization",
"data_need_graph_has_clarification_gaps"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const periodToken = "\u043f\u0435\u0440\u0438\u043e\u0434";
const organizationToken = "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446";
const counterpartyToken = "\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442";
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain(periodToken);
expect(draft.headline).toContain(organizationToken);
expect(draft.next_step_line).toContain(periodToken);
expect(draft.next_step_line).toContain(organizationToken);
expect(draft.next_step_line).not.toContain(counterpartyToken);
expect(draft.unknown_lines).toEqual([]);
expect(draft.limitation_lines).toEqual([]);
});
it("asks for organization rather than counterparty on open bidirectional comparison when only the period is known", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("входящий и исходящий");
expect(draft.next_step_line).toContain("организацию");
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
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("контуров");
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("keeps metadata lane-choice clarification human-facing when planner selects it from data-need graph", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["НДС"],
business_fact_family: "metadata_surface",
action_family: "resolve_next_lane",
aggregation_need: null,
time_scope_need: null,
comparison_need: null,
ranking_need: null,
proof_expectation: "supporting_evidence",
clarification_gaps: ["lane_family_choice"],
decomposition_candidates: [],
forbidden_overclaim_flags: ["no_raw_model_claims"],
reason_codes: ["data_need_graph_built", "data_need_graph_requires_lane_family_choice"]
},
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["НДС"],
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("контуров");
expect(draft.next_step_line).toContain("по документам");
expect(draft.next_step_line).toContain("по движениям/регистрам");
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
it("keeps movement clarification anchored to the chosen lane after metadata ambiguity was resolved", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["НДС"],
unsupported_but_understood_family: "movement_evidence"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("движениям/регистрам");
expect(draft.headline).toContain("НДС");
expect(draft.headline).toContain("период");
expect(draft.next_step_line).toContain("движениям/регистрам");
expect(draft.next_step_line).toContain("НДС");
expect(draft.next_step_line).toContain("период");
});
it("turns resolved entity grounding into a human-safe entity search answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Группа СВК"],
unsupported_but_understood_family: "entity_resolution"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" }
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("вероятный контрагент");
expect(draft.confirmed_lines.join("\n")).toContain("Группа СВК");
expect(draft.inference_lines.join("\n")).toContain("заземление сущности");
expect(draft.next_step_line).toContain("искать документы, движения или денежный поток");
expect(draft.must_not_claim).toContain(
"Do not present catalog grounding as confirmed business activity, turnover, or document evidence."
);
});
it("asks for clarification when entity grounding stays ambiguous", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК"],
unsupported_but_understood_family: "entity_resolution"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{ Counterparty: "СВК-А", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" }
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("несколько похожих контрагентов");
expect(draft.inference_lines.join("\n")).toContain("СВК-А");
expect(draft.inference_lines.join("\n")).toContain("1. СВК-А");
expect(draft.inference_lines.join("\n")).toContain("2. СВК-Б");
expect(draft.next_step_line).toContain("какой именно контрагент нужен");
expect(draft.next_step_line).toContain("1. СВК-А");
expect(draft.next_step_line).toContain("2. СВК-Б");
expect(draft.next_step_line).toContain("номером варианта");
});
it.skip("keeps entity search honest when no catalog candidate was confirmed", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Несуществующий Контрагент"],
unsupported_but_understood_family: "entity_resolution"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("checked_sources_only");
expect(draft.headline).toContain("точный контрагент пока не подтвержден");
expect(draft.unknown_lines).toContain(
'No counterparty matching "Несуществующий Контрагент" was confirmed in the checked 1C catalog slice'
);
expect(draft.next_step_line).toContain("Дайте точное название или ИНН");
});
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("схеме 1С");
expect(confirmedText).toContain("В схеме 1С");
expect(confirmedText).toContain("Документ.СчетФактураВыданный");
expect(confirmedText).toContain("Для следующего шага подходят");
expect(confirmedText).toContain("Дата");
expect(draft.inference_lines.join("\n")).toContain("контур документов");
expect(draft.next_step_line).toContain("типу «Документ»");
expect(confirmedText).not.toContain("Confirmed 1C metadata surface");
expect(confirmedText).not.toContain("Metadata surface family scores");
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("uses a distinct human headline for catalog drilldown instead of repeating a generic metadata overview", async () => {
const planner = planAssistantMcpDiscovery({
metadataSurface: {
selected_entity_set: "Catalog",
selected_surface_objects: ["Catalog.Counterparties"],
downstream_route_family: "catalog_drilldown",
route_family_selection_basis: "selected_entity_set",
recommended_next_primitive: "drilldown_related_objects",
ambiguity_detected: false,
ambiguity_entity_sets: []
},
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
unsupported_but_understood_family: "schema_surface"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildMetadataDeps([
{
FullName: "Catalog.Counterparties",
MetaType: "Catalog",
attributes: [{ Name: "Description" }]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("углубиться");
expect(draft.headline).toContain("справочников");
expect(draft.headline).not.toContain("catalog drilldown");
});
it("renders catalog metadata without leaking internal surface scoring", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
explicit_entity_candidates: ["контрагент"],
unsupported_but_understood_family: "1c_metadata_surface"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildMetadataDeps([
{ FullName: "Справочник.ДоговорыКонтрагентов", MetaType: "Справочник" },
{ FullName: "Справочник.Контрагенты", MetaType: "Справочник" }
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const userText = [
draft.headline,
...draft.confirmed_lines,
...draft.inference_lines,
...draft.unknown_lines,
draft.next_step_line ?? ""
].join("\n");
expect(userText).toContain("Справочник.ДоговорыКонтрагентов");
expect(userText).toContain("Справочник.Контрагенты");
expect(userText).toContain("Детальный список полей");
expect(userText).not.toContain("Confirmed 1C metadata surface");
expect(userText).not.toContain("Metadata surface family scores");
expect(userText).not.toContain("surface «");
expect(userText).not.toContain("catalog drilldown");
});
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("конкурирующих контуров");
expect(draft.inference_lines.join("\n")).toContain("несколько возможных контуров");
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("входящих денежных поступлений");
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"
);
expect(draft.unknown_lines).not.toContain(
"Complete requested-period coverage is not proven by the available checked rows"
);
});
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(pilot.evidence.inferred_facts).toContain(
"Activity window is bounded by first=2020-01-15, latest=2023-12-20, matched_rows=2"
);
expect(draft.reason_codes).toContain("pilot_derived_activity_period_from_confirmed_rows");
});
it("keeps not-found entity search user-facing lines in Russian", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["\u041d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"],
unsupported_but_understood_family: "entity_resolution"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a", CounterpartyRef: "Ref-1" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const unknownText = draft.unknown_lines.join("\n");
expect(draft.answer_mode).toBe("checked_sources_only");
expect(unknownText).toContain("\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
expect(unknownText).toContain("\u041d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
expect(unknownText).toContain("\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b, \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u0438 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438");
});
});