import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { useMcpKeysStore } from '@/stores/mcpKeys.js'
import McpKeysPanel from '@/components/settings/McpKeysPanel.vue'
vi.mock('@/api/index.js', () => ({
getMcpKeys: vi.fn(),
saveMcpKey: vi.fn(),
resetMcpKey: vi.fn(),
}))
vi.mock('@/composables/useConfirm.js', () => ({
useConfirm: () => () => Promise.resolve(true),
}))
// 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 ITEMS = [
{
server_name: 'http-server',
transport: 'streamable_http',
accepts_user_key: true,
key_type: 'header',
key_location: 'Authorization',
prefix: 'Bearer ',
has_key: false,
updated_at: null,
profiles: ['server_admin'],
},
{
server_name: 'stdio-server',
transport: 'stdio',
accepts_user_key: true,
key_type: 'env',
key_location: 'API_KEY',
prefix: null,
has_key: true,
updated_at: '2026-10-07T12:00:00Z',
profiles: ['discuss', 'server_admin'],
},
{
server_name: 'navi-web',
transport: 'stdio',
accepts_user_key: false,
key_type: null,
key_location: null,
prefix: null,
has_key: false,
updated_at: null,
profiles: ['discuss', 'secretary'],
},
]
describe('McpKeysPanel', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
// Fresh clone per test — the store mutates items in place.
api.getMcpKeys.mockImplementation(async () => ({ items: structuredClone(ITEMS) }))
api.saveMcpKey.mockImplementation(async (server, key) => ({
server_name: server, has_key: true, updated_at: '2026-10-08T00:00:00Z',
}))
api.resetMcpKey.mockResolvedValue(undefined)
})
it('renders every connected server, keyed ones with their key location', async () => {
const wrapper = mount(McpKeysPanel)
await new Promise(r => setTimeout(r))
expect(wrapper.text()).toContain('MCP servers')
expect(wrapper.text()).toContain('http-server')
expect(wrapper.text()).toContain('stdio-server')
expect(wrapper.text()).toContain('Authorization header (Bearer …)')
expect(wrapper.text()).toContain('API_KEY env')
// Keyless servers are listed too, with the profile they serve.
expect(wrapper.text()).toContain('navi-web')
expect(wrapper.text()).toContain('discuss, secretary')
expect(wrapper.text()).toContain('No personal key slot')
expect(wrapper.findAll('.mcp-keys-row')).toHaveLength(3)
})
it('offers a key input only for servers that declare a slot', async () => {
const wrapper = mount(McpKeysPanel)
await new Promise(r => setTimeout(r))
// Three rows, two of them with a slot → exactly two password inputs.
expect(wrapper.findAll('input[type="password"]')).toHaveLength(2)
const keylessRow = wrapper.findAll('.mcp-keys-row').find(r => r.text().includes('navi-web'))
expect(keylessRow.classes()).toContain('is-keyless')
expect(keylessRow.find('input').exists()).toBe(false)
// A keyed server without a saved key says the shared one is in use.
const httpRow = wrapper.findAll('.mcp-keys-row').find(r => r.text().includes('http-server'))
expect(httpRow.text()).toContain('the shared key from the config is used')
const stdioRow = wrapper.findAll('.mcp-keys-row').find(r => r.text().includes('stdio-server'))
expect(stdioRow.text()).toContain('Personal key set')
})
it('shows an empty-state message when no server is connected', async () => {
api.getMcpKeys.mockResolvedValue({ items: [] })
const wrapper = mount(McpKeysPanel)
await new Promise(r => setTimeout(r))
expect(wrapper.find('.mcp-keys-empty').exists()).toBe(true)
expect(wrapper.text()).toContain('No MCP servers are connected to any profile')
expect(wrapper.find('.mcp-keys-row').exists()).toBe(false)
})
it('saving a key PUTs it and marks the server as keyed', async () => {
const wrapper = mount(McpKeysPanel)
await new Promise(r => setTimeout(r))
const input = wrapper.findAll('input').find(i => i.attributes('type') === 'password')
input.element.value = 'sk-user'
await input.trigger('input')
const saveBtn = wrapper
.findAll('button')
.find(b => b.text().includes('Save'))
await saveBtn.trigger('click')
await new Promise(r => setTimeout(r))
expect(api.saveMcpKey).toHaveBeenCalledWith('http-server', 'sk-user')
const store = useMcpKeysStore()
expect(store.servers[0].has_key).toBe(true)
// input cleared after save
expect(input.element.value).toBe('')
})
it('reset asks for confirmation and DELETEs the saved key', async () => {
const wrapper = mount(McpKeysPanel)
await new Promise(r => setTimeout(r))
const resetBtn = wrapper
.findAll('button')
.find(b => b.text().includes('Reset to default'))
expect(resetBtn).toBeTruthy()
await resetBtn.trigger('click')
await new Promise(r => setTimeout(r))
expect(api.resetMcpKey).toHaveBeenCalledWith('stdio-server')
const store = useMcpKeysStore()
expect(store.servers[1].has_key).toBe(false)
})
})