358 lines
16 KiB
TypeScript
358 lines
16 KiB
TypeScript
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"
|
||
);
|
||
});
|
||
});
|