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

330 lines
14 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 };
}
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.inference_lines[0]).toContain("меньше месяца");
expect(draft.inference_lines.join("\n")).toContain("Первая найденная активность: 2020-01-15");
expect(draft.inference_lines.join("\n")).toContain("не юридически подтвержденный возраст регистрации");
expect(draft.unknown_lines).toContain("Legal registration date 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("MCP fetch failed: timeout");
expect(draft.next_step_line).toContain("MCP");
expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty.");
});
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("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("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 after the broad probe hit the row limit"
);
expect(draft.unknown_lines).not.toContain(
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
);
});
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(draft.reason_codes).toContain("pilot_derived_activity_period_from_confirmed_rows");
});
});