Newer
Older
navi-1 / webclient / tests / unit / components / settings / adminToolsPanel.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import AdminToolsPanel from '@/components/settings/AdminToolsPanel.vue'

vi.mock('@/api/index.js', () => ({
  reloadTools: vi.fn(),
}))

// No GnToastProvider in tests — swallow toast calls.
vi.mock('gnexus-ui-kit/vue', async (importOriginal) => {
  const mod = await importOriginal()
  const toast = {
    show: () => {}, info: () => {}, success: () => {},
    warning: () => {}, danger: () => {}, error: () => {}, close: () => {},
  }
  return { ...mod, useToast: () => toast }
})

import * as api from '@/api/index.js'

const OK_REPORT = {
  ok: true,
  tools: ['gmail', 'weather'],
  total_tools: 5,
  tool_errors: {},
  missing_enabled: [],
  providers: ['clock'],
  provider_errors: {},
  mcp_tools: ['mcp__alpha__ping'],
  mcp_error: null,
}

function button(wrapper) {
  return wrapper.findAll('button').find(b => b.text().includes('Reload'))
}

describe('AdminToolsPanel', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    api.reloadTools.mockResolvedValue(structuredClone(OK_REPORT))
  })

  it('renders the button and no report before it is pressed', () => {
    const wrapper = mount(AdminToolsPanel)
    expect(button(wrapper)).toBeTruthy()
    expect(wrapper.find('.admin-tools-report').exists()).toBe(false)
    expect(api.reloadTools).not.toHaveBeenCalled()
  })

  it('posts the reload and lists what came back', async () => {
    const wrapper = mount(AdminToolsPanel)

    await button(wrapper).trigger('click')
    await new Promise(r => setTimeout(r))

    expect(api.reloadTools).toHaveBeenCalledTimes(1)
    const report = wrapper.find('.admin-tools-report')
    expect(report.exists()).toBe(true)
    expect(report.classes()).not.toContain('has-problems')
    expect(report.text()).toContain('2 tools loaded')
    expect(report.text()).toContain('gmail, weather')
    expect(report.text()).toContain('5 in the registry')
    expect(report.text()).toContain('1 MCP tool')
    expect(report.text()).toContain('1 context provider')
  })

  it('flags a name in enabled.json that no tool answers to', async () => {
    api.reloadTools.mockResolvedValue({ ...structuredClone(OK_REPORT), missing_enabled: ['gmail'] })
    const wrapper = mount(AdminToolsPanel)

    await button(wrapper).trigger('click')
    await new Promise(r => setTimeout(r))

    const warning = wrapper.find('.admin-tools-line.is-warning')
    expect(warning.exists()).toBe(true)
    expect(warning.text()).toContain('gmail')
    expect(warning.text()).toContain('dropped from every profile')
  })

  it('shows a per-file error and marks the report as failed', async () => {
    api.reloadTools.mockResolvedValue({
      ...structuredClone(OK_REPORT),
      ok: false,
      tool_errors: { 'broken.py': 'SyntaxError: bad input' },
    })
    const wrapper = mount(AdminToolsPanel)

    await button(wrapper).trigger('click')
    await new Promise(r => setTimeout(r))

    expect(wrapper.find('.admin-tools-report').classes()).toContain('has-problems')
    const error = wrapper.find('.admin-tools-line.is-error')
    expect(error.text()).toContain('broken.py')
    expect(error.text()).toContain('SyntaxError: bad input')
  })

  it('reports an MCP failure instead of a tool count', async () => {
    api.reloadTools.mockResolvedValue({
      ...structuredClone(OK_REPORT),
      ok: false,
      mcp_tools: [],
      mcp_error: 'RuntimeError: mcp is down',
    })
    const wrapper = mount(AdminToolsPanel)

    await button(wrapper).trigger('click')
    await new Promise(r => setTimeout(r))

    expect(wrapper.text()).toContain('MCP reload failed: RuntimeError: mcp is down')
    expect(wrapper.text()).not.toContain('0 MCP tools')
  })

  it('surfaces a rejected request and keeps the old report untouched', async () => {
    api.reloadTools.mockRejectedValue(new Error('POST /admin/tools/reload → 403'))
    const wrapper = mount(AdminToolsPanel)

    await button(wrapper).trigger('click')
    await new Promise(r => setTimeout(r))

    expect(wrapper.find('.admin-tools-report').exists()).toBe(false)
  })

  it('disables the button while the reload is in flight', async () => {
    let release
    api.reloadTools.mockImplementation(() => new Promise(r => { release = r }))
    const wrapper = mount(AdminToolsPanel)

    await button(wrapper).trigger('click')
    expect(button(wrapper).attributes('disabled')).toBeDefined()
    expect(button(wrapper).text()).toContain('Reloading')

    release(structuredClone(OK_REPORT))
    await new Promise(r => setTimeout(r))
    expect(button(wrapper).attributes('disabled')).toBeUndefined()
  })
})