NODEDC_1C/llm_normalizer/backend/tests/assistantMcpDiscoveryPolicy...

142 lines
6.1 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 {
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);
});
});