|
| 1 | +import { beforeEach, describe, expect, it, vi } from "vitest"; |
| 2 | +import { modelVisibleClinicalSchema, prepareClinicalMcpArguments } from "./clinical-mcp-broker"; |
| 3 | +import * as patientCases from "./patient-cases-store"; |
| 4 | +import * as backend from "./shared-backend-client"; |
| 5 | + |
| 6 | +vi.mock("./patient-cases-store"); |
| 7 | +vi.mock("./shared-backend-client"); |
| 8 | + |
| 9 | +const policy = { entryId: "10000000-0000-4000-8000-000000000001", organizationId: "10000000-0000-4000-8000-000000000002", allowedTools: "*" as const, dataEgressPolicy: "unrestricted" as const, integrationProfile: "modelforge-clinical" as const }; |
| 10 | + |
| 11 | +describe("clinical MCP broker", () => { |
| 12 | + beforeEach(() => vi.clearAllMocks()); |
| 13 | + |
| 14 | + it("removes infrastructure-only fields from model-visible tool schemas", () => { |
| 15 | + expect(modelVisibleClinicalSchema({ type: "object", properties: { contextGrantId: {}, approvalTicket: {}, idempotencyKey: {}, rationale: {} }, required: ["contextGrantId", "approvalTicket", "idempotencyKey", "rationale"] })).toEqual({ type: "object", properties: { rationale: {} }, required: ["rationale"] }); |
| 16 | + }); |
| 17 | + |
| 18 | + it("hides authoritative medication fields without mutating the wire schema", () => { |
| 19 | + const schema = { type: "object", additionalProperties: false, properties: { medications: {}, allergies: {}, contextGrantId: {} }, required: ["medications", "allergies", "contextGrantId"] }; |
| 20 | + expect(modelVisibleClinicalSchema(schema, "clinical.medication_conflict_check")).toEqual({ type: "object", additionalProperties: false, properties: {}, required: [] }); |
| 21 | + expect(schema.required).toEqual(["medications", "allergies", "contextGrantId"]); |
| 22 | + expect(Object.keys(schema.properties)).toEqual(["medications", "allergies", "contextGrantId"]); |
| 23 | + expect(modelVisibleClinicalSchema(undefined, "clinical.medication_conflict_check")).toBeUndefined(); |
| 24 | + }); |
| 25 | + |
| 26 | + it("does not remove similarly named domain fields from other tools", () => { |
| 27 | + const schema = { properties: { medications: {} }, required: ["medications"] }; |
| 28 | + expect(modelVisibleClinicalSchema(schema, "clinical.response_contract_check")).toEqual(schema); |
| 29 | + }); |
| 30 | + |
| 31 | + it("rejects missing or excluded case data before requesting a grant", async () => { |
| 32 | + await expect(prepareClinicalMcpArguments(policy, "clinical.medication_conflict_check", {})).rejects.toThrow(/Attach a patient case/); |
| 33 | + vi.mocked(patientCases.getCase).mockResolvedValue(null); |
| 34 | + await expect(prepareClinicalMcpArguments(policy, "clinical.medication_conflict_check", {}, { patientCaseId: "case-1" })).rejects.toThrow(/no longer available/); |
| 35 | + vi.mocked(patientCases.getCase).mockResolvedValue({ medications: { includeInContext: false, value: [] }, allergies: { includeInContext: true, value: [] } } as never); |
| 36 | + await expect(prepareClinicalMcpArguments(policy, "clinical.medication_conflict_check", {}, { patientCaseId: "case-1" })).rejects.toThrow(/Include both/); |
| 37 | + expect(backend.createMcpContextGrant).not.toHaveBeenCalled(); |
| 38 | + }); |
| 39 | + |
| 40 | + it("uses medications and allergies from the attached case and injects only the grant handle", async () => { |
| 41 | + vi.mocked(patientCases.getCase).mockResolvedValue({ medications: { includeInContext: true, value: ["warfarin"] }, allergies: { includeInContext: true, value: ["aspirin"] } } as never); |
| 42 | + vi.mocked(backend.createMcpContextGrant).mockResolvedValue({ id: "grant-1" } as never); |
| 43 | + await expect(prepareClinicalMcpArguments(policy, "clinical.medication_conflict_check", { medications: ["untrusted"] }, { patientCaseId: "case-1" })).resolves.toEqual({ medications: ["warfarin"], allergies: ["aspirin"], contextGrantId: "grant-1" }); |
| 44 | + expect(backend.createMcpContextGrant).toHaveBeenCalledWith(expect.objectContaining({ caseId: "case-1", requestedFields: ["allergies", "medications"] })); |
| 45 | + }); |
| 46 | + |
| 47 | + it("requires a human-approved review and injects an operation-bound ticket and idempotency key", async () => { |
| 48 | + vi.mocked(backend.createMcpContextGrant).mockResolvedValue({ id: "grant-2" } as never); |
| 49 | + vi.mocked(backend.prepareMcpApproval).mockResolvedValue({ approvalRequest: { id: "10000000-0000-4000-8000-000000000003" }, challenge: {} } as never); |
| 50 | + vi.mocked(backend.confirmMcpApproval).mockResolvedValue({ approvalRequest: {}, approvalTicket: "ticket-1" } as never); |
| 51 | + const args = { reviewedOperationId: "10000000-0000-4000-8000-000000000004", decision: "approved", rationale: "Checked." }; |
| 52 | + await expect(prepareClinicalMcpArguments(policy, "clinical.record_review_decision", args, { patientCaseId: "case-1" })).rejects.toThrow(/explicit approval/); |
| 53 | + expect(backend.createMcpContextGrant).not.toHaveBeenCalled(); |
| 54 | + expect(backend.prepareMcpApproval).not.toHaveBeenCalled(); |
| 55 | + expect(backend.confirmMcpApproval).not.toHaveBeenCalled(); |
| 56 | + const result = await prepareClinicalMcpArguments(policy, "clinical.record_review_decision", args, { patientCaseId: "case-1", humanApproved: true }); |
| 57 | + expect(result).toMatchObject({ ...args, contextGrantId: "grant-2", approvalTicket: "ticket-1" }); |
| 58 | + expect(result.idempotencyKey).toEqual(expect.any(String)); |
| 59 | + }); |
| 60 | + |
| 61 | + it("strips model-provided infrastructure credentials and leaves generic tools unchanged", async () => { |
| 62 | + vi.mocked(backend.createMcpContextGrant).mockResolvedValue({ id: "trusted-grant" } as never); |
| 63 | + const args = { assistantResponse: "draft", contextGrantId: "model-grant", approvalTicket: "model-ticket", idempotencyKey: "model-key" }; |
| 64 | + await expect(prepareClinicalMcpArguments(policy, "clinical.response_contract_check", args, { patientCaseId: "case-1" })).resolves.toEqual({ assistantResponse: "draft", contextGrantId: "trusted-grant" }); |
| 65 | + await expect(prepareClinicalMcpArguments({ ...policy, integrationProfile: "generic" }, "generic.tool", args)).resolves.toBe(args); |
| 66 | + }); |
| 67 | +}); |
0 commit comments