import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { useAuthStore } from '@/stores/auth.js'
import SkillsPanel from '@/components/settings/SkillsPanel.vue'
vi.mock('@/api/index.js', () => ({
getSkills: vi.fn(),
getSkill: vi.fn(),
createSkill: vi.fn(),
updateSkill: vi.fn(),
deleteSkill: vi.fn(),
getSkillVersions: 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 LIST = [
{
id: 'sk-1', kind: 'skill', name: 'Разбор логов',
description: 'Быстрый поиск причины в логах.',
area: 'workflows', tags: ['logs'], visibility: 'personal',
version: 2, owner_id: 'u1', can_edit: true,
},
{
id: 'pr-1', kind: 'protocol', name: 'Выключение',
description: 'Порядок выключения.',
area: 'infrastructure', tags: ['power'], visibility: 'shared',
version: 1, owner_id: '', can_edit: false,
},
]
const FULL_SKILL = {
...LIST[0],
content: '---\nkind: skill\nname: Разбор логов\n---\n\nГрепай ERROR.\n',
}
const PROTOCOL_DOC = '---\nkind: protocol\nname: Выключение\n---\n'
function settle() {
return new Promise(r => setTimeout(r))
}
describe('SkillsPanel', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
useAuthStore().user = { id: 'u1', role: 'user', permissions: [] }
api.getSkills.mockResolvedValue({ items: structuredClone(LIST) })
api.getSkill.mockImplementation(async (id) => (
id === 'pr-1'
? { ...structuredClone(LIST[1]), content: PROTOCOL_DOC }
: structuredClone(FULL_SKILL)
))
api.getSkillVersions.mockResolvedValue({ items: [] })
})
it('lists both kinds with their kind, area and visibility', async () => {
const wrapper = mount(SkillsPanel)
await settle()
expect(api.getSkills).toHaveBeenCalled()
expect(wrapper.findAll('.skills-row')).toHaveLength(2)
expect(wrapper.text()).toContain('Разбор логов')
expect(wrapper.text()).toContain('protocol')
expect(wrapper.text()).toContain('Выключение')
})
it('filters are sent to the API', async () => {
const wrapper = mount(SkillsPanel)
await settle()
api.getSkills.mockClear()
// The kind select is the first one in the toolbar.
const select = wrapper.findAll('select')[0]
await select.setValue('protocol')
await settle()
await new Promise(r => setTimeout(r, 300))
expect(api.getSkills).toHaveBeenCalledWith(
expect.objectContaining({ kind: 'protocol' }),
)
})
it('opens an entry and shows the server document', async () => {
const wrapper = mount(SkillsPanel)
await settle()
await wrapper.findAll('.skills-row')[0].trigger('click')
await settle()
expect(api.getSkill).toHaveBeenCalledWith('sk-1')
expect(wrapper.find('textarea').element.value).toContain('Грепай ERROR')
expect(wrapper.find('.skills-editor-title').text()).toContain('Разбор логов')
})
it('a shared entry is read-only for a regular user', async () => {
const wrapper = mount(SkillsPanel)
await settle()
await wrapper.findAll('.skills-row')[1].trigger('click')
await settle()
expect(wrapper.find('.skills-readonly').exists()).toBe(true)
expect(wrapper.find('textarea').attributes('disabled')).toBeDefined()
// No visibility control: only an admin can move an entry between scopes.
expect(wrapper.findAll('select')).toHaveLength(2)
})
it('an admin can choose the visibility', async () => {
useAuthStore().user = { id: 'u1', role: 'admin', permissions: [] }
const wrapper = mount(SkillsPanel)
await settle()
await wrapper.findAll('.skills-row')[0].trigger('click')
await settle()
// Two toolbar filters plus the editor's visibility select.
expect(wrapper.findAll('select')).toHaveLength(3)
})
it('shows the server validation message when a protocol is refused', async () => {
api.createSkill.mockRejectedValue(
new Error('POST /skills → 400: {"detail":"Protocol needs: post_checks, on_blocked"}'),
)
const wrapper = mount(SkillsPanel)
await settle()
const newProtocol = wrapper.findAll('button').find(b => b.text().includes('New protocol'))
await newProtocol.trigger('click')
await settle()
const save = wrapper.findAll('button').find(b => b.text().trim() === 'Save')
await save.trigger('click')
await settle()
expect(api.createSkill).toHaveBeenCalled()
expect(wrapper.find('.skills-save-error').text()).toContain(
'Protocol needs: post_checks, on_blocked',
)
})
it('a new protocol starts from a template with the escape hatches', async () => {
const wrapper = mount(SkillsPanel)
await settle()
const newProtocol = wrapper.findAll('button').find(b => b.text().includes('New protocol'))
await newProtocol.trigger('click')
await settle()
const text = wrapper.find('textarea').element.value
expect(text).toContain('kind: protocol')
expect(text).toContain('on_deviation:')
expect(text).toContain('post_checks:')
})
})