import { describe, expect, it } from "vitest"; import { buildAssistantMcpDiscoveryPlan, isAssistantMcpDiscoveryPrimitive, resolveAssistantMcpDiscoveryEvidence } from "../src/services/assistantMcpDiscoveryPolicy"; describe("assistant MCP discovery policy", () => { it("allows guarded MCP primitives and keeps raw model claims outside the answer path", () => { const plan = buildAssistantMcpDiscoveryPlan({ semanticDataNeed: "counterparty turnover evidence", turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_entity_candidates: ["Группа СВК"], explicit_date_scope: "2020" }, proposedPrimitives: ["resolve_entity_reference", "query_movements", "drop_database"], requiredAxes: ["counterparty", "period", "amount"], maxProbeCount: 99, maxRowsPerProbe: 9999 }); expect(plan.plan_status).toBe("allowed"); expect(plan.allowed_primitives).toEqual(["resolve_entity_reference", "query_movements"]); expect(plan.rejected_primitives).toEqual(["drop_database"]); expect(plan.requires_evidence_gate).toBe(true); expect(plan.answer_may_use_raw_model_claims).toBe(false); expect(plan.execution_budget).toEqual({ max_probe_count: 36, max_rows_per_probe: 500 }); expect(plan.reason_codes).toContain("model_proposed_unregistered_mcp_primitive"); }); it("blocks model-planned probes when no proposed primitive survives the runtime allowlist", () => { const plan = buildAssistantMcpDiscoveryPlan({ semanticDataNeed: "direct SQL from model", turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_entity_candidates: ["СВК"] }, proposedPrimitives: ["raw_sql", "filesystem_read"], requiredAxes: ["counterparty"] }); expect(plan.plan_status).toBe("blocked"); expect(plan.allowed_primitives).toEqual([]); expect(plan.rejected_primitives).toEqual(["raw_sql", "filesystem_read"]); expect(plan.reason_codes).toContain("no_allowed_mcp_primitives_after_runtime_filter"); }); it("separates confirmed, inferred and unknown facts before answer composition", () => { const plan = buildAssistantMcpDiscoveryPlan({ semanticDataNeed: "activity duration from 1C evidence", turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["СВК"] }, proposedPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], requiredAxes: ["counterparty", "document_date"] }); const evidence = resolveAssistantMcpDiscoveryEvidence({ plan, probeResults: [ { primitive_id: "resolve_entity_reference", status: "ok", rows_received: 1, rows_matched: 1 }, { primitive_id: "query_documents", status: "ok", rows_received: 4, rows_matched: 4 } ], confirmedFacts: ["first confirmed 1C activity is 2020-01-15"], inferredFacts: ["activity duration can be estimated from first and latest 1C activity"], unknownFacts: ["legal registration date is not proven by these rows"], sourceRowsSummary: "5 allowed MCP rows: 1 entity match, 4 documents" }); expect(evidence.evidence_status).toBe("confirmed"); expect(evidence.coverage_status).toBe("full"); expect(evidence.answer_permission).toBe("confirmed_answer"); expect(evidence.confirmed_facts).toHaveLength(1); expect(evidence.inferred_facts).toHaveLength(1); expect(evidence.unknown_facts).toHaveLength(1); expect(evidence.reason_codes).toContain("confirmed_facts_with_allowed_mcp_evidence"); }); it("permits only bounded inference when probes found rows but no confirmed fact", () => { const plan = buildAssistantMcpDiscoveryPlan({ semanticDataNeed: "counterparty business age inference", turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "age_or_activity_duration", explicit_entity_candidates: ["СВК"] }, proposedPrimitives: ["query_documents"], requiredAxes: ["counterparty", "document_date"] }); const evidence = resolveAssistantMcpDiscoveryEvidence({ plan, probeResults: [{ primitive_id: "query_documents", status: "ok", rows_received: 3, rows_matched: 0 }], inferredFacts: ["activity is visible in 1C documents for 2020"], unknownFacts: ["legal age remains unknown"], sourceRowsSummary: "3 document rows checked" }); expect(evidence.evidence_status).toBe("inferred_only"); expect(evidence.coverage_status).toBe("partial"); expect(evidence.answer_permission).toBe("bounded_inference"); expect(evidence.confidence_reason).toBe("only_inferred_facts_available_from_allowed_mcp_probe_rows"); }); it("blocks evidence when execution reports a primitive outside the runtime plan", () => { const plan = buildAssistantMcpDiscoveryPlan({ semanticDataNeed: "counterparty turnover evidence", turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_entity_candidates: ["СВК"] }, proposedPrimitives: ["query_movements"], requiredAxes: ["counterparty"] }); const evidence = resolveAssistantMcpDiscoveryEvidence({ plan, probeResults: [ { primitive_id: "query_movements", status: "ok", rows_received: 2, rows_matched: 2 }, { primitive_id: "raw_sql", status: "ok", rows_received: 10, rows_matched: 10 } ], confirmedFacts: ["turnover is 100"], sourceRowsSummary: "12 rows" }); expect(evidence.evidence_status).toBe("blocked"); expect(evidence.answer_permission).toBe("checked_sources_only"); expect(evidence.reason_codes).toContain("probe_result_used_primitive_outside_runtime_plan"); }); it("exports the reviewed primitive predicate for future runtime adapters", () => { expect(isAssistantMcpDiscoveryPrimitive("query_movements")).toBe(true); expect(isAssistantMcpDiscoveryPrimitive("raw_sql")).toBe(false); }); });