botProfileDialog.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  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('textarea').setValue('新的规则')
  42. await view.get('.profile-save').trigger('click')
  43. expect(view.emitted('save')).toEqual([[{ name: '新的名称', avatar: '', instructions: '新的规则', source: '["local","ops/team"]', canManageTeam: false, canCollaborate: true, memoryEnabled: true, approvalPolicy: 'ask' }]])
  44. expect(agent.name).toBe('目标机器人')
  45. })
  46. it('uses the saved target Profile for model services and blocks an unsaved source switch', async () => {
  47. const view = open()
  48. await page('模型与 Provider').trigger('click')
  49. expect(view.findComponent({ name: 'ModelServicesPanel' }).props('profile')).toBe('ops/team')
  50. await view.get('select[aria-label="基础机器人"]').setValue('["local","default"]')
  51. expect(view.findComponent({ name: 'ModelServicesPanel' }).exists()).toBe(false)
  52. expect(view.text()).toContain('请先保存基础机器人的变更')
  53. })
  54. it.each([false, true])('saves a per-Bot approval policy, including remote references (remote=%s)', async remote => {
  55. const view = open({ agent: { ...agent, ...(remote ? { remoteAgentId: 'remote-bot' } : {}) } })
  56. await page('权限').trigger('click')
  57. expect(view.get<HTMLInputElement>('input[value="ask"]').element.checked).toBe(true)
  58. await view.get('input[value="deny"]').setValue()
  59. await view.get('.profile-save').trigger('click')
  60. expect(view.emitted('save')?.[0]?.[0]).toMatchObject({ approvalPolicy: 'deny' })
  61. })
  62. 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 => {
  63. const view = open(overrides)
  64. if (page('模型与 Provider')) await page('模型与 Provider').trigger('click')
  65. expect(view.findComponent({ name: 'ModelServicesPanel' }).exists()).toBe(false)
  66. })
  67. it('pins memory to this Bot and protects unsaved memory and Provider edits', async () => {
  68. const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false)
  69. const view = open()
  70. await page('身份与头像').trigger('click')
  71. await view.get('input[maxlength="100"]').setValue('改过的名称')
  72. await page('记忆').trigger('click')
  73. const memory = view.findComponent({ name: 'WorkspaceKnowledgePanel' })
  74. expect(memory.props()).toMatchObject({ selectedAgentId: 'target-bot', conversationId: 'target-chat', initialTab: 'agent', hideAgentControls: true })
  75. memory.vm.$emit('dirty-change', true)
  76. await view.vm.$nextTick()
  77. await page('模型与 Provider').trigger('click')
  78. expect(confirm).toHaveBeenCalledWith('放弃当前记忆未保存的更改?')
  79. expect(view.findComponent({ name: 'WorkspaceKnowledgePanel' }).exists()).toBe(true)
  80. memory.vm.$emit('dirty-change', false)
  81. await view.vm.$nextTick()
  82. await page('模型与 Provider').trigger('click')
  83. view.findComponent({ name: 'ModelServicesPanel' }).vm.$emit('dirty-change', true)
  84. await view.vm.$nextTick()
  85. expect(view.get<HTMLButtonElement>('.profile-save').element.disabled).toBe(true)
  86. await page('身份与头像').trigger('click')
  87. expect(confirm).toHaveBeenCalledWith('放弃当前模型服务未保存的更改?')
  88. expect(view.findComponent({ name: 'ModelServicesPanel' }).exists()).toBe(true)
  89. })