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