serviceWarnings.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. import { afterEach, expect, it, vi } from 'vitest'
  2. import { flushPromises, mount } from '@vue/test-utils'
  3. import { createPinia } from 'pinia'
  4. import WorkspaceTimeline from '@/components/workspace/WorkspaceMessageTimeline.vue'
  5. import MessageTimeline from '@/components/messages/MessageTimeline.vue'
  6. import { workspaceMessagesToUi } from '@/components/workspace/viewModels'
  7. import { WorkspaceTranscriptStore } from '@/components/workspace/transcriptStore'
  8. import { buildMessageTimelineRows } from '@/utils/turnTrace'
  9. import type { WorkspaceMessage } from '@shared/workspace'
  10. const warning={code:'plugin_unavailable',service:'天气服务',message:'连接暂不可用,本次任务继续使用其他能力。'}
  11. const message:WorkspaceMessage={id:'warning-message',conversationId:'chat',agentId:'bot',agentName:'竹儿',role:'assistant',content:'',reasoning:'',status:'complete',seq:1,createdAt:1,tools:[{id:'tool',name:'天气服务',status:'tool.complete'}],attachments:[],serviceWarnings:[warning]}
  12. afterEach(()=>{vi.restoreAllMocks();vi.unstubAllGlobals()})
  13. it('keeps warning-only messages visible independently of tool trace aggregation and model text',()=>{
  14. const [ui]=workspaceMessagesToUi([JSON.parse(JSON.stringify(message))])
  15. expect(ui?.serviceWarnings).toEqual([warning])
  16. expect(ui?.status).toBe('settled')
  17. expect(ui?.error).toBeUndefined()
  18. const rows=buildMessageTimelineRows([ui!])
  19. expect(rows.map(row=>row.kind)).toEqual(['trace','message'])
  20. expect(rows[1]?.kind==='message'&&rows[1].message.serviceWarnings).toEqual([warning])
  21. const [textOnly]=workspaceMessagesToUi([{...message,serviceWarnings:undefined,content:JSON.stringify({serviceWarnings:[warning]})}])
  22. expect(textOnly?.serviceWarnings).toBeUndefined()
  23. })
  24. it.each(['workspace','session'])('shows non-blocking, escaped service notices with tools hidden in the %s timeline',async(timeline)=>{
  25. vi.stubGlobal('ResizeObserver',class {observe(){}disconnect(){}})
  26. const scrollTo=HTMLElement.prototype.scrollTo
  27. HTMLElement.prototype.scrollTo=vi.fn()
  28. const unsafe={code:'plugin_unavailable',service:'<img src=x onerror="alert(1)">',message:'<script>alert(2)</script> [修复](javascript:alert(3))'}
  29. const messages=workspaceMessagesToUi([{...message,serviceWarnings:[warning,unsafe]}])
  30. const wrapper=mount(timeline==='workspace'?WorkspaceTimeline:MessageTimeline,{props:{identity:'chat',messages,showTools:false},global:{plugins:[createPinia()],stubs:{AppIcon:true,AgentAvatar:true,MarkdownContent:{props:['content'],template:'<div class="fixture-content">{{content}}</div>'}}}})
  31. try{
  32. await flushPromises()
  33. const notices=wrapper.get('[aria-label="服务状态提示"]')
  34. expect(notices.findAll('[role="status"]')).toHaveLength(2)
  35. expect(notices.text()).toContain(`${warning.service}:${warning.message}`)
  36. expect(notices.text()).toContain(`${unsafe.service}:${unsafe.message}`)
  37. expect(notices.find('img,script,a,[role="alert"],details').exists()).toBe(false)
  38. expect(wrapper.find('.turn-trace').exists()).toBe(false)
  39. expect(wrapper.find('.message-failure').exists()).toBe(false)
  40. await wrapper.setProps({messages:workspaceMessagesToUi([{...message,status:'streaming',content:'正在继续处理任务。'}])})
  41. expect(wrapper.text()).toContain('正在继续处理任务。')
  42. expect(wrapper.get('[aria-label="服务状态提示"]').text()).toContain(warning.message)
  43. }finally{wrapper.unmount();HTMLElement.prototype.scrollTo=scrollTo}
  44. })
  45. it('retains service warnings from live transcript events through a history reload',()=>{
  46. const conversation={id:'chat',kind:'direct',memberIds:['bot'],lastSeq:1} as any
  47. const store=new WorkspaceTranscriptStore()
  48. store.hydrate({agents:[],conversations:[conversation],details:[{conversation,messages:[],cursor:0,hasOlder:false,run:null,interactions:[],context:null}],cursor:0})
  49. store.apply({type:'message.changed',seq:1,conversationId:'chat',data:message})
  50. expect(workspaceMessagesToUi(store.get('chat')!.messages)[0]?.serviceWarnings).toEqual([warning])
  51. const refreshed=new WorkspaceTranscriptStore()
  52. refreshed.hydrate({agents:[],conversations:[conversation],details:[JSON.parse(JSON.stringify(store.get('chat')))],cursor:1})
  53. const messages=workspaceMessagesToUi(refreshed.get('chat')!.messages)
  54. expect(messages[0]?.serviceWarnings).toEqual([warning])
  55. expect(buildMessageTimelineRows(messages).some(row=>row.kind==='message'&&row.message.id===message.id)).toBe(true)
  56. })