import { describe, it, expect, beforeEach, vi } from 'vitest' import { MCP_TOOL_CAPABILITY_MAP, PAID_OPERATION_CAPABILITY_MAP, PAID_CAPABILITIES, CAPABILITY, } from '../keys' /** * These maps are the contract that gates the paid MCP/agent path (dispatch + * commit). Locking the exact entries is the guard against a future paid * external-service tool silently bypassing the paywall: mirrors the * TOOL_SCOPE_MAP assertions in the mcp-server tests. */ beforeEach(() => { vi.clearAllMocks() }) /** * MCP tools that invoke a paid capability directly (no stage→commit round-trip), * so they are gated at DISPATCH only and have no commit-time (operation-map) * counterpart. The document upload tools run Bedrock OCR inline via * extractInvoiceFields: they never stage a pending_operation. */ const DISPATCH_ONLY_MCP_TOOLS = new Set([ 'gnubok_upload_document', 'gnubok_create_document_upload', 'gnubok_complete_document_upload', // Onboarding connect-link tools: read status + hand out a browser link; no commit counterpart. 'gnubok_connect_bank', 'gnubok_connect_skatteverket', // Agent-triggered PSD2 sync: inline Enable Banking call, no staged operation. 'gnubok_sync_bank', ]) describe('MCP_TOOL_CAPABILITY_MAP', () => { it('gates exactly the paid MCP tools (3 external-service staging tools + the AI OCR tools)', () => { expect(MCP_TOOL_CAPABILITY_MAP).toEqual({ gnubok_send_invoice: CAPABILITY.email_send, gnubok_vat_declaration_submit: CAPABILITY.skatteverket, gnubok_agi_submit: CAPABILITY.skatteverket, gnubok_connect_bank: CAPABILITY.bank_sync, gnubok_sync_bank: CAPABILITY.bank_sync, gnubok_connect_skatteverket: CAPABILITY.skatteverket, // Dispatch-only AI tools: inline Bedrock OCR, no staged operation. The // signed-URL pair is gated at create AND complete so a free-tier key can // neither reserve nor finalize a paid extraction. gnubok_upload_document: CAPABILITY.ai, gnubok_create_document_upload: CAPABILITY.ai, gnubok_complete_document_upload: CAPABILITY.ai, }) }) it('only maps tools to PAID capabilities', () => { for (const key of Object.values(MCP_TOOL_CAPABILITY_MAP)) { expect(PAID_CAPABILITIES).toContain(key) } }) }) describe('PAID_OPERATION_CAPABILITY_MAP', () => { it('gates exactly the three paid pending-operation types', () => { expect(PAID_OPERATION_CAPABILITY_MAP).toEqual({ send_invoice: CAPABILITY.email_send, submit_vat_declaration: CAPABILITY.skatteverket, submit_agi: CAPABILITY.skatteverket, }) }) it('only maps operations to PAID capabilities', () => { for (const key of Object.values(PAID_OPERATION_CAPABILITY_MAP)) { expect(PAID_CAPABILITIES).toContain(key) } }) it('covers the same capabilities as the STAGING MCP tools (dispatch ↔ commit parity)', () => { // Parity applies to staging tools only: an op that can be staged via MCP OR // approved in the UI must be gated on both transports. Dispatch-only tools // (inline AI OCR) have no commit counterpart and are excluded. const stagingMcpCaps = new Set( Object.entries(MCP_TOOL_CAPABILITY_MAP) .filter(([tool]) => !DISPATCH_ONLY_MCP_TOOLS.has(tool)) .map(([, cap]) => cap), ) expect(new Set(Object.values(PAID_OPERATION_CAPABILITY_MAP))).toEqual(stagingMcpCaps) }) }) describe('CONNECTOR_CAPABILITIES', () => { it('names only real capability keys, with the two connector-only keys outside the paid set', async () => { const { CAPABILITY, CONNECTOR_CAPABILITIES, PAID_CAPABILITIES, isConnectorCapability } = await import('../keys') const all = new Set(Object.values(CAPABILITY)) for (const key of CONNECTOR_CAPABILITIES) expect(all.has(key), key).toBe(true) expect(CONNECTOR_CAPABILITIES).toEqual(['bank_sync', 'skatteverket', 'org_lookup', 'migration']) // org_lookup and migration stay free on hosted (not PAID) but still need // Accounted's services, hence connector-gated on a self-host. expect(PAID_CAPABILITIES).not.toContain('org_lookup') expect(PAID_CAPABILITIES).not.toContain('migration') expect(isConnectorCapability('ai')).toBe(false) expect(isConnectorCapability('bank_sync')).toBe(true) }) })