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();
  });
});