336 lines
15 KiB
TypeScript
336 lines
15 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph";
|
||
|
||
describe("assistant MCP discovery data need graph", () => {
|
||
it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "counterparty value-flow evidence",
|
||
rawUtterance: "какое нетто по деньгам с SVK за 2020 год по месяцам",
|
||
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.business_fact_family).toBe("value_flow");
|
||
expect(result.action_family).toBe("net_value_flow");
|
||
expect(result.aggregation_need).toBe("by_month");
|
||
expect(result.time_scope_need).toBe("explicit_period");
|
||
expect(result.comparison_need).toBe("incoming_vs_outgoing");
|
||
expect(result.proof_expectation).toBe("coverage_checked_fact");
|
||
expect(result.clarification_gaps).toEqual([]);
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"resolve_entity_reference",
|
||
"collect_incoming_movements",
|
||
"collect_outgoing_movements",
|
||
"aggregate_by_month",
|
||
"probe_coverage"
|
||
]);
|
||
expect(result.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals");
|
||
});
|
||
|
||
it("marks metadata lane choice as a clarification-required graph", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "metadata lane clarification",
|
||
rawUtterance: "давай дальше",
|
||
turnMeaning: {
|
||
asked_domain_family: "metadata",
|
||
asked_action_family: "resolve_next_lane",
|
||
explicit_entity_candidates: ["SVK"],
|
||
unsupported_but_understood_family: "metadata_lane_choice_clarification"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("schema_surface");
|
||
expect(result.clarification_gaps).toEqual(["lane_family_choice"]);
|
||
expect(result.proof_expectation).toBe("clarification_required");
|
||
});
|
||
|
||
it("keeps entity search as an entity-grounding graph", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "entity discovery evidence",
|
||
rawUtterance: "найди в 1С контрагента Группа СВК",
|
||
turnMeaning: {
|
||
asked_domain_family: "entity_resolution",
|
||
asked_action_family: "search_business_entity",
|
||
explicit_entity_candidates: ["Группа СВК"]
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("entity_grounding");
|
||
expect(result.subject_candidates).toEqual(["Группа СВК"]);
|
||
expect(result.proof_expectation).toBe("entity_grounding");
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"search_business_entity",
|
||
"resolve_entity_reference",
|
||
"probe_coverage"
|
||
]);
|
||
expect(result.forbidden_overclaim_flags).toContain("no_unresolved_entity_claim");
|
||
});
|
||
|
||
it("builds a subjectless inventory stock snapshot graph with an as-of date gate", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "inventory stock snapshot evidence",
|
||
rawUtterance: "show stock snapshot for warehouse main as of 2021-09-30",
|
||
turnMeaning: {
|
||
asked_domain_family: "inventory_stock",
|
||
asked_action_family: "stock_snapshot",
|
||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||
explicit_date_scope: "2021-09-30"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("inventory_stock_snapshot");
|
||
expect(result.subject_candidates).toEqual([]);
|
||
expect(result.clarification_gaps).toEqual([]);
|
||
expect(result.time_scope_need).toBe("explicit_period");
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"fetch_scoped_movements",
|
||
"aggregate_checked_amounts",
|
||
"probe_coverage",
|
||
"explain_evidence_basis"
|
||
]);
|
||
expect(result.forbidden_overclaim_flags).toContain("no_unchecked_stock_snapshot");
|
||
});
|
||
|
||
it("keeps inventory stock snapshot blocked on an explicit as-of date when no date exists", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "inventory stock snapshot evidence",
|
||
rawUtterance: "show current stock snapshot",
|
||
turnMeaning: {
|
||
asked_domain_family: "inventory_stock",
|
||
asked_action_family: "stock_snapshot",
|
||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("inventory_stock_snapshot");
|
||
expect(result.clarification_gaps).toEqual(["as_of_date"]);
|
||
expect(result.proof_expectation).toBe("clarification_required");
|
||
});
|
||
|
||
it("builds inventory provenance and sale-trace graphs as checked document drilldowns", () => {
|
||
const provenance = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "inventory purchase provenance evidence",
|
||
rawUtterance: "who supplied the selected stock item",
|
||
turnMeaning: {
|
||
asked_domain_family: "inventory_stock",
|
||
asked_action_family: "purchase_provenance",
|
||
explicit_entity_candidates: ["Столешница 600"]
|
||
}
|
||
});
|
||
const saleTrace = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "inventory sale trace evidence",
|
||
rawUtterance: "who bought the selected stock item",
|
||
turnMeaning: {
|
||
asked_domain_family: "inventory_stock",
|
||
asked_action_family: "sale_trace",
|
||
explicit_entity_candidates: ["Столешница 600"]
|
||
}
|
||
});
|
||
|
||
expect(provenance.business_fact_family).toBe("inventory_purchase_provenance");
|
||
expect(provenance.decomposition_candidates).toEqual([
|
||
"resolve_entity_reference",
|
||
"fetch_scoped_documents",
|
||
"drilldown_related_objects",
|
||
"probe_coverage",
|
||
"explain_evidence_basis"
|
||
]);
|
||
expect(provenance.forbidden_overclaim_flags).toContain("no_unproven_supplier_attribution");
|
||
expect(saleTrace.business_fact_family).toBe("inventory_sale_trace");
|
||
expect(saleTrace.decomposition_candidates).toEqual([
|
||
"resolve_entity_reference",
|
||
"fetch_scoped_documents",
|
||
"drilldown_related_objects",
|
||
"probe_coverage",
|
||
"explain_evidence_basis"
|
||
]);
|
||
expect(saleTrace.forbidden_overclaim_flags).toContain("no_unproven_buyer_or_sale_trace");
|
||
});
|
||
|
||
it("treats top-value wording as a ranking ask rather than a missing-subject fact ask", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "counterparty value-flow evidence",
|
||
rawUtterance: "кто больше всего принес денег в 2020",
|
||
turnMeaning: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
explicit_date_scope: "2020"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("value_flow");
|
||
expect(result.ranking_need).toBe("top_desc");
|
||
expect(result.clarification_gaps).toEqual(["organization"]);
|
||
expect(result.proof_expectation).toBe("clarification_required");
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"collect_scoped_movements",
|
||
"aggregate_ranked_axis_values",
|
||
"probe_coverage"
|
||
]);
|
||
expect(result.reason_codes).toContain("data_need_graph_ranking_top_desc");
|
||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
|
||
});
|
||
|
||
it("keeps organization-scoped ranking executable when the ranking axis comes from follow-up context", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "counterparty value-flow evidence",
|
||
rawUtterance: "по ООО Альтернатива Плюс",
|
||
turnMeaning: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||
explicit_date_scope: "2020",
|
||
seeded_ranking_need: "top_desc"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("value_flow");
|
||
expect(result.ranking_need).toBe("top_desc");
|
||
expect(result.clarification_gaps).toEqual([]);
|
||
expect(result.proof_expectation).toBe("coverage_checked_fact");
|
||
});
|
||
|
||
it("treats incoming-vs-outgoing comparison as an open-scope value need rather than a missing-subject fact ask", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "counterparty value-flow evidence",
|
||
rawUtterance: "что больше: входящие или исходящие деньги за 2020 год?",
|
||
turnMeaning: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "net_value_flow",
|
||
explicit_date_scope: "2020"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("value_flow");
|
||
expect(result.comparison_need).toBe("incoming_vs_outgoing");
|
||
expect(result.clarification_gaps).toEqual(["organization"]);
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"collect_incoming_movements",
|
||
"collect_outgoing_movements",
|
||
"probe_coverage"
|
||
]);
|
||
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
|
||
});
|
||
it("treats organization-scoped incoming totals as an open-scope value need rather than a missing-subject fact ask", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "counterparty value-flow evidence",
|
||
rawUtterance: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
|
||
turnMeaning: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
explicit_date_scope: "2020",
|
||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("value_flow");
|
||
expect(result.comparison_need).toBeNull();
|
||
expect(result.ranking_need).toBeNull();
|
||
expect(result.clarification_gaps).toEqual([]);
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"collect_scoped_movements",
|
||
"aggregate_checked_amounts",
|
||
"probe_coverage"
|
||
]);
|
||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
|
||
});
|
||
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "counterparty value-flow evidence",
|
||
rawUtterance: "сколько входящих денег за 2020 год?",
|
||
turnMeaning: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
explicit_date_scope: "2020"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("value_flow");
|
||
expect(result.subject_candidates).toEqual([]);
|
||
expect(result.clarification_gaps).toEqual(["organization"]);
|
||
expect(result.proof_expectation).toBe("clarification_required");
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"collect_scoped_movements",
|
||
"aggregate_checked_amounts",
|
||
"probe_coverage"
|
||
]);
|
||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
|
||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
|
||
});
|
||
|
||
it("treats colloquial open incoming total wording with filler words as an open-scope ask rather than a missing subject", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "counterparty value-flow evidence",
|
||
rawUtterance: "сколько вообще входящих денег было?",
|
||
turnMeaning: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("value_flow");
|
||
expect(result.subject_candidates).toEqual([]);
|
||
expect(result.clarification_gaps).toEqual(["organization", "period"]);
|
||
expect(result.proof_expectation).toBe("clarification_required");
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"collect_scoped_movements",
|
||
"aggregate_checked_amounts",
|
||
"probe_coverage"
|
||
]);
|
||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
|
||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
|
||
});
|
||
|
||
it("treats all-time open-scope totals as an open-ended period rather than a missing period", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "counterparty value-flow evidence",
|
||
rawUtterance: "сколько вообще денег мы заработали за все время?",
|
||
turnMeaning: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("value_flow");
|
||
expect(result.subject_candidates).toEqual([]);
|
||
expect(result.time_scope_need).toBe("all_time_scope");
|
||
expect(result.clarification_gaps).toEqual([]);
|
||
expect(result.proof_expectation).toBe("coverage_checked_fact");
|
||
expect(result.decomposition_candidates).toEqual([
|
||
"collect_scoped_movements",
|
||
"aggregate_checked_amounts",
|
||
"probe_coverage"
|
||
]);
|
||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
|
||
expect(result.reason_codes).toContain("data_need_graph_all_time_scope_hint");
|
||
});
|
||
|
||
it("treats metadata-scoped movement evidence as subjectless and asks only for organization plus period", () => {
|
||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||
semanticDataNeed: "movement evidence",
|
||
rawUtterance: "по движениям",
|
||
turnMeaning: {
|
||
asked_domain_family: "movements",
|
||
asked_action_family: "list_movements",
|
||
metadata_scope_hint: "НДС",
|
||
subject_resolution_optional: true,
|
||
unsupported_but_understood_family: "movement_evidence"
|
||
}
|
||
});
|
||
|
||
expect(result.business_fact_family).toBe("movement_evidence");
|
||
expect(result.subject_candidates).toEqual([]);
|
||
expect(result.metadata_scope_hint).toBe("НДС");
|
||
expect(result.subject_resolution_optional).toBe(true);
|
||
expect(result.clarification_gaps).toEqual(["organization", "period"]);
|
||
expect(result.decomposition_candidates).toEqual(["fetch_scoped_movements", "probe_coverage"]);
|
||
expect(result.reason_codes).toContain("data_need_graph_metadata_scoped_open_lane_without_subject");
|
||
});
|
||
});
|