NODEDC_1C/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanne...

516 lines
23 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 } from "vitest";
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
describe("assistant MCP discovery planner", () => {
it("builds a catalog-compatible value-flow discovery plan from current turn meaning", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
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: [
"resolve_entity_reference",
"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"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.selected_chain_summary).toContain("query scoped movements");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
expect(result.reason_codes).toContain("planner_consumed_data_need_graph_v1");
});
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["period", "counterparty"]);
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
it("keeps requested monthly aggregation as an explicit planning axis for value-flow discovery", () => {
const result = 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"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
expect(result.required_axes).toEqual([
"counterparty",
"period",
"aggregate_axis",
"amount",
"coverage_target",
"calendar_month"
]);
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_recipe");
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
});
it("builds a document discovery plan without falling back to movement primitives", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"]
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.proposed_primitives).not.toContain("query_movements");
expect(result.required_axes).toEqual(["counterparty", "coverage_target"]);
});
it("builds a movement discovery plan without aggregating value-flow totals", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["resolve_entity_reference", "fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.selected_chain_id).toBe("movement_evidence");
expect(result.selected_chain_summary).toContain("movement rows");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
expect(result.proposed_primitives).not.toContain("aggregate_by_axis");
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_movement_from_data_need_graph");
});
it("can select value-flow chain from data need graph even when turn meaning family is still under-specified", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: "by_month",
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: [
"resolve_entity_reference",
"collect_incoming_movements",
"collect_outgoing_movements",
"aggregate_by_month",
"probe_coverage"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
asked_aggregation_axis: "month"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
expect(result.required_axes).toEqual([
"counterparty",
"period",
"aggregate_axis",
"amount",
"coverage_target",
"calendar_month"
]);
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_from_data_need_graph");
});
it("does not collapse a ranking-shaped value graph into entity-resolution just because no subject is preselected", () => {
const result = 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"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("value_flow_ranking");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps ranked value-flow ready for execution once checked period and organization are known", () => {
const result = 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",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow_ranking");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
});
it("does not collapse incoming-vs-outgoing comparison into entity-resolution when no counterparty is preselected", () => {
const result = 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"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("value_flow_comparison");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "amount", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps bidirectional comparison ready for execution once checked period and organization are known", () => {
const result = 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",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow_comparison");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
});
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_documents",
"probe_coverage",
"explain_evidence_basis"
]);
expect(result.required_axes).toEqual(["counterparty", "document_date", "coverage_target", "evidence_basis"]);
});
it("uses metadata-only planning when the user asks about available schema surface", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.required_axes).toEqual(["metadata_scope"]);
expect(result.catalog_review.evidence_floors.inspect_1c_metadata).toBe("source_summary");
});
it("keeps broad metadata surface inspection on inspect_1c_metadata", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_surface",
explicit_entity_candidates: ["НДС"]
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("1C metadata evidence");
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.proposed_primitives).not.toContain("query_documents");
expect(result.proposed_primitives).not.toContain("query_movements");
expect(result.reason_codes).toContain("planner_selected_metadata_recipe");
});
it("keeps metadata document inspection on inspect_1c_metadata instead of query_documents", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.proposed_primitives).not.toContain("query_documents");
});
it("keeps metadata lane-choice clarification in needs_clarification without launching MCP primitives", () => {
const result = 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"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.semantic_data_need).toBe("metadata lane clarification");
expect(result.selected_chain_id).toBe("metadata_lane_clarification");
expect(result.selected_chain_summary).toContain("choose the next data lane");
expect(result.proposed_primitives).toEqual([]);
expect(result.required_axes).toEqual(["counterparty", "period", "lane_family_choice"]);
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
expect(result.reason_codes).toContain("planner_selected_metadata_lane_clarification_recipe");
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
it("does not mark an unclassified turn as executable without turn meaning context", () => {
const result = planAssistantMcpDiscovery({});
expect(result.planner_status).toBe("needs_clarification");
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
it("exposes an explicit entity-resolution chain instead of silently collapsing into a lifecycle lane", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
explicit_entity_candidates: ["SVK"]
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("entity discovery evidence");
expect(result.selected_chain_id).toBe("entity_resolution");
expect(result.selected_chain_summary).toContain("resolve the most relevant 1C reference");
expect(result.proposed_primitives).toEqual(["search_business_entity", "resolve_entity_reference", "probe_coverage"]);
});
it("keeps organization-scoped one-sided value-flow totals executable without forcing a counterparty", () => {
const result = 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: 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: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("organization-scoped value-flow evidence");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
});
it("keeps generic one-sided open totals in organization clarification instead of forcing entity resolution", () => {
const result = 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: 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"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "organization", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["organization"]);
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
});