mcp-tool-checklist.test.tsx6.9 KBView on GitHub import { fireEvent, render, screen, within } from '@testing-library/react';
import {
McpReviewPermissionsBanner,
McpToolChecklist,
} from '@/modules/integrations/mcp-tool-checklist';
import type { McpToolRow } from '@/modules/integrations/mcp-tool-policy';
const row = (over: Partial<McpToolRow> = {}): McpToolRow => ({
name: 'create_payment_link',
description: 'Create a payment link',
allowed: false,
...over,
});
const noop = () => {};
describe('McpToolChecklist', () => {
it('shows an unticked tool as denied, in words and not just by an empty box', () => {
render(
<McpToolChecklist tools={[row()]} onToggle={noop} onInstructionChange={noop} />,
);
const toolRow = screen.getByTestId('mcp-tool-row-create_payment_link');
expect(within(toolRow).getByText('Denied')).toBeInTheDocument();
expect(within(toolRow).getByText('The agent cannot call this tool.')).toBeInTheDocument();
expect(within(toolRow).queryByText('Allowed')).not.toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: 'Allow create_payment_link' })).not.toBeChecked();
});
it('states the deny-by-default rule once, at the top', () => {
render(<McpToolChecklist tools={[row()]} onToggle={noop} onInstructionChange={noop} />);
expect(screen.getByText(/Unticked tools are denied/i)).toBeInTheDocument();
});
it('badges a tool that is present on the server but absent from the saved rules', () => {
render(
<McpToolChecklist
tools={[row({ name: 'delete_everything', isNew: true })]}
onToggle={noop}
onInstructionChange={noop}
/>,
);
const toolRow = screen.getByTestId('mcp-tool-row-delete_everything');
expect(within(toolRow).getByText('New — not enabled')).toBeInTheDocument();
// A new tool is also, necessarily, denied — it must not inherit an old tick.
expect(within(toolRow).getByText('Denied')).toBeInTheDocument();
expect(screen.getByText('1 new since last review')).toBeInTheDocument();
});
it('does not badge a tool that has a saved rule', () => {
render(<McpToolChecklist tools={[row({ allowed: true })]} onToggle={noop} onInstructionChange={noop} />);
expect(screen.queryByText('New — not enabled')).not.toBeInTheDocument();
});
it('offers the per-tool instruction only once the tool is allowed', () => {
const { rerender } = render(
<McpToolChecklist tools={[row()]} onToggle={noop} onInstructionChange={noop} />,
);
expect(
screen.queryByLabelText('Instruction for create_payment_link'),
).not.toBeInTheDocument();
rerender(
<McpToolChecklist
tools={[row({ allowed: true, instruction: 'Annual plans only.' })]}
onToggle={noop}
onInstructionChange={noop}
/>,
);
expect(screen.getByLabelText('Instruction for create_payment_link')).toHaveValue(
'Annual plans only.',
);
});
it('reports a tick and an instruction edit back to the owner of the state', () => {
const onToggle = jest.fn();
const onInstructionChange = jest.fn();
render(
<McpToolChecklist
tools={[row({ allowed: true })]}
onToggle={onToggle}
onInstructionChange={onInstructionChange}
/>,
);
fireEvent.click(screen.getByRole('checkbox', { name: 'Allow create_payment_link' }));
expect(onToggle).toHaveBeenCalledWith('create_payment_link', false);
fireEvent.change(screen.getByLabelText('Instruction for create_payment_link'), {
target: { value: 'Annual only' },
});
expect(onInstructionChange).toHaveBeenCalledWith('create_payment_link', 'Annual only');
});
it('renders argument constraints and pinned arguments as read-only chips', () => {
render(
<McpToolChecklist
tools={[
row({
name: 'stripe_api_write',
allowed: true,
argumentRules: [{ field: 'stripe_api_operation_id', oneOf: ['PostPaymentLinks'] }],
pinnedArguments: { livemode: false },
}),
]}
onToggle={noop}
onInstructionChange={noop}
/>,
);
expect(
screen.getByText('stripe_api_operation_id is one of PostPaymentLinks'),
).toBeInTheDocument();
expect(screen.getByText(/pinned:\s*livemode = false/)).toBeInTheDocument();
});
it('shows the approval toggle only when the surface can save one', () => {
const onRequireApprovalChange = jest.fn();
const { rerender } = render(
<McpToolChecklist
tools={[row({ allowed: true })]}
onToggle={noop}
onInstructionChange={noop}
/>,
);
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
rerender(
<McpToolChecklist
tools={[row({ allowed: true })]}
onToggle={noop}
onInstructionChange={noop}
onRequireApprovalChange={onRequireApprovalChange}
/>,
);
fireEvent.click(screen.getByRole('switch'));
expect(onRequireApprovalChange).toHaveBeenCalledWith('create_payment_link', true);
});
it('renders no stray "0" for a tool with an empty argument-rule list', () => {
const toolRow = render(
<McpToolChecklist
tools={[row({ allowed: true, argumentRules: [] })]}
onToggle={noop}
onInstructionChange={noop}
/>,
).getByTestId('mcp-tool-row-create_payment_link');
expect(toolRow.textContent).not.toMatch(/(^|\s)0(\s|$)/);
});
it('renders an empty state rather than an empty list', () => {
render(
<McpToolChecklist
tools={[]}
onToggle={noop}
onInstructionChange={noop}
emptyMessage="This server reported no tools."
/>,
);
expect(screen.getByText('This server reported no tools.')).toBeInTheDocument();
});
});
describe('McpReviewPermissionsBanner', () => {
it('warns on an allow_all connection that the agent can call every tool', () => {
render(
<McpReviewPermissionsBanner
policy={{ mode: 'allow_all', allowedCount: 0, ruleCount: 0 }}
/>,
);
expect(screen.getByRole('status')).toHaveTextContent(/Review permissions/i);
expect(screen.getByRole('status')).toHaveTextContent(/can call every tool/i);
});
it('renders nothing for an allowlist connection', () => {
const { container } = render(
<McpReviewPermissionsBanner
policy={{ mode: 'allowlist', allowedCount: 2, ruleCount: 5 }}
/>,
);
expect(container).toBeEmptyDOMElement();
});
it('renders nothing when the digest is absent', () => {
const { container } = render(<McpReviewPermissionsBanner policy={null} />);
expect(container).toBeEmptyDOMElement();
});
it('opens the permissions section from the banner', () => {
const onReview = jest.fn();
render(
<McpReviewPermissionsBanner
policy={{ mode: 'allow_all', allowedCount: 0, ruleCount: 0 }}
onReview={onReview}
/>,
);
fireEvent.click(screen.getByRole('button', { name: 'Review' }));
expect(onReview).toHaveBeenCalled();
});
});