1097 lines
49 KiB
TypeScript
1097 lines
49 KiB
TypeScript
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("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");
|
||
});
|
||
});
|