NODEDC_1C/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntim...

358 lines
16 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 { runAssistantMcpDiscoveryRuntimeBridge } from "../src/services/assistantMcpDiscoveryRuntimeBridge";
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 buildBidirectionalDeps(
incomingRows: Array<Record<string, unknown>>,
outgoingRows: Array<Record<string, unknown>>
) {
return {
executeAddressMcpQuery: vi
.fn()
.mockResolvedValueOnce({
fetched_rows: incomingRows.length,
matched_rows: incomingRows.length,
raw_rows: incomingRows,
rows: incomingRows,
error: null
})
.mockResolvedValueOnce({
fetched_rows: outgoingRows.length,
matched_rows: outgoingRows.length,
raw_rows: outgoingRows,
rows: outgoingRows,
error: null
})
};
}
describe("assistant MCP discovery runtime bridge", () => {
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
},
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
});
expect(result.schema_version).toBe("assistant_mcp_discovery_runtime_bridge_v1");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.hot_runtime_wired).toBe(false);
expect(result.pilot.mcp_execution_performed).toBe(true);
expect(result.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.user_facing_response_allowed).toBe(true);
expect(result.reason_codes).toContain("runtime_bridge_not_wired_to_hot_assistant_answer");
});
it("keeps missing scope as clarification and does not authorize a business fact answer", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.business_fact_answer_allowed).toBe(false);
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
});
it("keeps ranked value-flow in clarification without asking for a counterparty when only the period is known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
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"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
expect(result.answer_draft.headline).toContain("ranking");
expect(result.answer_draft.next_step_line).toContain("организацию");
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
});
it("produces a bounded ranked value-flow answer when period and organization are known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
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",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildDeps([
{ Period: "2020-01-10T00:00:00", Amount: 1200, Counterparty: "СВК-А" },
{ Period: "2020-03-11T00:00:00", Amount: 800, Counterparty: "СВК-Б" },
{ Period: "2020-05-12T00:00:00", Amount: 900, Counterparty: "СВК-А" }
])
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
expect(result.pilot.derived_ranked_value_flow?.ranked_values[0]).toMatchObject({
axis_value: "СВК-А",
total_amount: 2100
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
});
it("keeps open bidirectional comparison in clarification without asking for a counterparty when only the period is known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
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"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.answer_draft.headline).toContain("входящий и исходящий");
expect(result.answer_draft.next_step_line).toContain("организацию");
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
});
it("produces a bounded bidirectional comparison answer when period and organization are known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
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",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildBidirectionalDeps(
[
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "СВК-А" },
{ Period: "2020-04-11T00:00:00", Amount: 1800, Counterparty: "СВК-Б" }
],
[{ Period: "2020-02-12T00:00:00", Amount: 1400, Counterparty: "СВК-А" }]
)
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
period_scope: "2020",
incoming_customer_revenue: {
total_amount: 5000
},
outgoing_supplier_payout: {
total_amount: 1400
}
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("получили");
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("заплатили");
});
it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "document",
asked_action_family: "documents",
explicit_entity_candidates: ["SVK"]
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("checked_sources_only");
expect(result.hot_runtime_wired).toBe(false);
expect(result.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
expect(result.pilot.mcp_execution_performed).toBe(true);
expect(result.business_fact_answer_allowed).toBe(false);
expect(result.reason_codes).toContain("runtime_bridge_status_checked_sources_only");
});
it("preserves the answer adapter boundary against internal mechanics leakage", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
},
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
});
const userFacing = [
result.answer_draft.headline,
...result.answer_draft.confirmed_lines,
...result.answer_draft.inference_lines,
...result.answer_draft.unknown_lines,
...result.answer_draft.limitation_lines,
result.answer_draft.next_step_line ?? ""
].join("\n");
expect(userFacing).not.toContain("query_documents");
expect(userFacing).not.toContain("runtime_bridge");
expect(userFacing).not.toContain("primitive");
});
it("produces a bounded one-sided value-flow answer for an organization-scoped total without inventing a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
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: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "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"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildDeps([
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "Клиент-А" },
{ Period: "2020-05-22T00:00:00", Amount: 1800, Counterparty: "Клиент-Б" }
])
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow");
expect(result.pilot.derived_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
total_amount: 5000
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000");
expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту");
});
it("keeps generic one-sided open totals in organization clarification without asking for a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
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: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization"],
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "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"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow");
expect(result.answer_draft.next_step_line).toContain("\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e");
expect(result.answer_draft.next_step_line).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
);
});
});