feat(sync): 添加 Vault 所有的用户 Skill 记录

This commit is contained in:
2026-09-09 08:39:14 +08:00
parent bdd1543a4d
commit b0783c9356
25 changed files with 1507 additions and 29 deletions
@@ -0,0 +1,97 @@
// @vitest-environment happy-dom
import { beforeEach, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import UserSkillEditor from './UserSkillEditor.vue'
import { useWorkspaceStore } from '@/stores/workspace'
import { useSkillStore } from '@/stores/skill'
import * as service from '@/services/skillService'
import type { UserSkill } from '@/contracts'
vi.mock('@/services/skillService', () => ({
listSkills: vi.fn(), listUserSkills: vi.fn(), createUserSkill: vi.fn(), updateUserSkill: vi.fn(), deleteUserSkill: vi.fn(),
}))
const saved: UserSkill = {
skill_id: 'user_skill_' + '1'.repeat(32), revision: 'a'.repeat(64), status: 'ready',
missing_dependencies: [], undeclared_permissions: [],
data: { version: 1, name: 'Review', description: '', prompt: 'Review carefully', tools: ['notes.read'], permissions: ['notes.read'], retrieval: { top_k: 10, rerank: true, citation: true }, required_capabilities: ['chat'], created_at_ms: 1, updated_at_ms: 1 },
}
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
useWorkspaceStore().vaultId = 'vault-one'
vi.mocked(service.listSkills).mockResolvedValue([])
vi.mocked(service.listUserSkills).mockResolvedValue([])
vi.mocked(service.createUserSkill).mockResolvedValue(saved)
vi.mocked(service.updateUserSkill).mockResolvedValue({ ...saved, revision: 'b'.repeat(64), data: { ...saved.data, version: 2 } })
vi.mocked(service.deleteUserSkill).mockResolvedValue({ status: 'completed' })
})
it('creates a complete declarative record and labels declarations as non-grants', async () => {
const wrapper = mount(UserSkillEditor)
await wrapper.findAll('input.input')[0]!.setValue('Review')
await wrapper.findAll('input.input')[1]!.setValue('notes.read')
await wrapper.get('textarea').setValue('Review carefully')
await wrapper.get('input[value="notes.read"]').setValue(true)
await wrapper.get('input[value="chat"]').setValue(true)
await wrapper.get('form').trigger('submit')
await flushPromises()
expect(service.createUserSkill).toHaveBeenCalledWith(expect.objectContaining({
revision: '', name: 'Review', prompt: 'Review carefully', tools: ['notes.read'],
permissions: ['notes.read'], required_capabilities: ['chat'],
retrieval: { top_k: 10, rerank: true, citation: true },
}), expect.stringMatching(/^[0-9a-f-]{36}$/))
expect(wrapper.text()).toContain('不是设备授权')
expect(useSkillStore().userSkills).toHaveLength(1)
wrapper.unmount()
})
it('does not publish a late save response into a different Vault', async () => {
let finish!: (value: UserSkill) => void
vi.mocked(service.createUserSkill).mockImplementation(() => new Promise(resolve => { finish = resolve }))
const wrapper = mount(UserSkillEditor)
await wrapper.findAll('input.input')[0]!.setValue('Review')
await wrapper.get('form').trigger('submit')
useWorkspaceStore().vaultId = 'vault-two'
await wrapper.vm.$nextTick()
finish(saved)
await flushPromises()
expect(useSkillStore().userSkills).toEqual([])
expect(wrapper.text()).toContain('WORKSPACE_CHANGED')
wrapper.unmount()
})
it('does not publish a late list response into a different Vault', async () => {
let finish!: (value: UserSkill[]) => void
vi.mocked(service.listUserSkills).mockImplementation(() => new Promise(resolve => { finish = resolve }))
const store = useSkillStore()
const loading = store.loadSkills()
useWorkspaceStore().vaultId = 'vault-two'
finish([saved])
await loading
expect(store.userSkills).toEqual([])
})
it('reuses the operation UUID after an ambiguous save failure and changes it with the payload', async () => {
vi.mocked(service.createUserSkill)
.mockRejectedValueOnce(new Error('HOST_TIMEOUT'))
.mockResolvedValueOnce(saved)
.mockRejectedValueOnce(new Error('HOST_TIMEOUT'))
const wrapper = mount(UserSkillEditor)
const name = wrapper.findAll('input.input')[0]!
await name.setValue('Review')
await wrapper.get('form').trigger('submit')
await flushPromises()
const firstOperation = vi.mocked(service.createUserSkill).mock.calls[0]![1]
await wrapper.get('form').trigger('submit')
await flushPromises()
expect(vi.mocked(service.createUserSkill).mock.calls[1]![1]).toBe(firstOperation)
await wrapper.findAll('button').find(button => button.text().includes('清空表单'))!.trigger('click')
await name.setValue('Changed')
await wrapper.get('form').trigger('submit')
await flushPromises()
expect(vi.mocked(service.createUserSkill).mock.calls[2]![1]).not.toBe(firstOperation)
wrapper.unmount()
})