botProfileDialog.test.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { mount, type VueWrapper } from '@vue/test-utils'
  2. import { afterEach, expect, it, vi } from 'vitest'
  3. import BotProfileDialog from '@/components/workspace/BotProfileDialog.vue'
  4. import type { WorkspaceAgent } from '@shared/workspace'
  5. const agent: WorkspaceAgent = { id: 'target-bot', name: '目标机器人', avatar: '', instructions: '检查边界条件', nodeId: 'local', profile: 'ops/team', execution: 'computer', canManageTeam: false, canCollaborate: true, memoryEnabled: true, archived: false, revision: 1, createdAt: 1, updatedAt: 1 }
  6. let wrapper: VueWrapper | undefined
  7. function open(overrides: Record<string, unknown> = {}) {
  8. wrapper = mount(BotProfileDialog, {
  9. props: {
  10. agent,
  11. draft: { ...agent, source: JSON.stringify(['local', 'ops/team']) },
  12. sources: [{ nodeId: 'local', profile: 'ops/team', name: '运维机器人' }, { nodeId: 'local', profile: 'default', name: '丫头' }],
  13. profiles: [{ name: 'default', agentName: '丫头', isDefault: true }],
  14. agents: [agent], conversations: [], conversationId: 'target-chat', isAdmin: true, knowledgeEnabled: true,
  15. ...overrides,
  16. } as never,
  17. global: { stubs: {
  18. StandaloneDialog: { props: ['title', 'beforeClose'], template: '<div role="dialog" :aria-label="title"><slot /></div>' },
  19. AppIcon: true, AgentAvatar: true, AgentIdentityPanel: true,
  20. ModelServicesPanel: true, WorkspaceKnowledgePanel: true,
  21. } },
  22. })
  23. return wrapper
  24. }
  25. function page(label: string) { return wrapper!.findAll('nav button').find(button => button.text() === label)! }
  26. afterEach(() => { wrapper?.unmount(); vi.restoreAllMocks() })
  27. it('shows the associated Profile identity instead of repeating its technical name', () => {
  28. const view = open({ sources: [{ nodeId: 'local', profile: 'ops/team', name: 'ops/team' }], profiles: [{ name: 'ops/team', agentName: '运维助手', isDefault: false }] })
  29. expect(view.get('.profile-source__identity').text()).toBe('运维助手ops/team')
  30. })
  31. it('keeps identity drafts on the right-clicked Bot while the account has another Profile', async () => {
  32. const view = open()
  33. expect(view.get('[role="dialog"]').attributes('aria-label')).toBe('编辑资料 · 目标机器人')
  34. expect(view.get<HTMLButtonElement>('.profile-save').element.disabled).toBe(true)
  35. await page('身份与头像').trigger('click')
  36. const identity = view.findComponent({ name: 'AgentIdentityPanel' })
  37. expect(identity.props('profile')).toMatchObject({ name: 'target-bot', agentName: '目标机器人' })
  38. expect(identity.props('showDefaultModel')).toBe(false)
  39. await view.get('input[maxlength="100"]').setValue('新的名称')
  40. await page('角色与规则').trigger('click')
  41. await view.get('.persona-advanced summary').trigger('click')
  42. await view.get('textarea[maxlength="24000"]').setValue('新的规则')
  43. await view.get('.profile-save').trigger('click')
  44. expect(view.emitted('save')?.[0]?.[0]).toMatchObject({ name: '新的名称', avatar: '', instructions: '新的规则', voice: '', actBias: '', source: '["local","ops/team"]', canManageTeam: false, canCollaborate: true, memoryEnabled: true, approvalPolicy: 'ask' })
  45. expect(agent.name).toBe('目标机器人')
  46. })
  47. it('uses the same selectable-card treatment for tone and action preference', async () => {
  48. const view = open()
  49. await page('角色与规则').trigger('click')
  50. const groups = view.findAll('.persona-choice-group')
  51. expect(groups).toHaveLength(2)
  52. expect(groups[0].findAll('.radio-row')).toHaveLength(5)
  53. expect(groups[1].findAll('.radio-row')).toHaveLength(4)
  54. expect(groups[0].get('.radio-row.selected').text()).toContain('未指定')
  55. expect(groups[1].get('.radio-row.selected').text()).toContain('未指定')
  56. await groups[0].find('input[value="rigorous"]').setValue(true)
  57. await groups[1].find('input[value="act_now"]').setValue(true)
  58. expect(groups[0].get('.radio-row.selected').text()).toContain('严谨细致')
  59. expect(groups[1].get('.radio-row.selected').text()).toContain('直接行动')
  60. })
  61. it('uses the saved target Profile for model services and blocks an unsaved source switch', async () => {
  62. const view = open()
  63. await page('模型与 Provider').trigger('click')
  64. expect(view.findComponent({ name: 'ModelServicesPanel' }).props('profile')).toBe('ops/team')
  65. await view.get('select[aria-label="基础机器人"]').setValue('["local","default"]')
  66. expect(view.findComponent({ name: 'ModelServicesPanel' }).exists()).toBe(false)
  67. expect(view.text()).toContain('请先保存基础机器人的变更')
  68. })
  69. it.each([false, true])('shows shared policy without a per-Bot override (remote=%s)', async remote => {
  70. const view = open({ agent: { ...agent, approvalPolicy:'deny', ...(remote ? { remoteAgentId: 'remote-bot' } : {}) } })
  71. await page('权限').trigger('click')
  72. expect(view.find('input[value="ask"]').exists()).toBe(false)
  73. expect(view.text()).toContain('所有 Bot')
  74. expect(view.text()).toContain('自动拒绝')
  75. expect(view.emitted('save')).toBeUndefined()
  76. })
  77. it.each([{ isAdmin: false }, { agent: { ...agent, nodeId: 'remote' } }, { agent: { ...agent, remoteAgentId: 'remote-bot' } }])('does not expose local model management outside the eligible Bot scope: %j', async overrides => {
  78. const view = open(overrides)
  79. if (page('模型与 Provider')) await page('模型与 Provider').trigger('click')
  80. expect(view.findComponent({ name: 'ModelServicesPanel' }).exists()).toBe(false)
  81. })
  82. it('pins memory to this Bot and protects unsaved memory and Provider edits', async () => {
  83. const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false)
  84. const view = open()
  85. await page('身份与头像').trigger('click')
  86. await view.get('input[maxlength="100"]').setValue('改过的名称')
  87. await page('记忆').trigger('click')
  88. const memory = view.findComponent({ name: 'WorkspaceKnowledgePanel' })
  89. expect(memory.props()).toMatchObject({ selectedAgentId: 'target-bot', conversationId: 'target-chat', initialTab: 'agent', hideAgentControls: true })
  90. memory.vm.$emit('dirty-change', true)
  91. await view.vm.$nextTick()
  92. await page('模型与 Provider').trigger('click')
  93. expect(confirm).toHaveBeenCalledWith('放弃当前记忆未保存的更改?')
  94. expect(view.findComponent({ name: 'WorkspaceKnowledgePanel' }).exists()).toBe(true)
  95. memory.vm.$emit('dirty-change', false)
  96. await view.vm.$nextTick()
  97. await page('模型与 Provider').trigger('click')
  98. view.findComponent({ name: 'ModelServicesPanel' }).vm.$emit('dirty-change', true)
  99. await view.vm.$nextTick()
  100. expect(view.get<HTMLButtonElement>('.profile-save').element.disabled).toBe(true)
  101. await page('身份与头像').trigger('click')
  102. expect(confirm).toHaveBeenCalledWith('放弃当前模型服务未保存的更改?')
  103. expect(view.findComponent({ name: 'ModelServicesPanel' }).exists()).toBe(true)
  104. })