messageFailure.test.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. import { describe, expect, it, vi } from 'vitest'
  2. import { mount } from '@vue/test-utils'
  3. import { collapseDuplicateFailures, messageFailure } from '@/utils/messageFailure'
  4. import MessageFailureNotice from '@/components/messages/MessageFailureNotice.vue'
  5. import MessageTimeline from '@/components/messages/MessageTimeline.vue'
  6. import WorkspaceMessageTimeline from '@/components/workspace/WorkspaceMessageTimeline.vue'
  7. import { workspaceMessagesToUi } from '@/components/workspace/viewModels'
  8. import type { UiMessage } from '@/components/messages/types'
  9. const error = 'Codex stream produced no SSE events for 120s after the first parsed event (threshold: 120s)'
  10. const failed: UiMessage = { id: 'answer', role: 'assistant', content: error, error, status: 'failed', runId: 'run', createdAt: 1000 }
  11. const duplicate: UiMessage = { id: 'system', role: 'system', content: `执行失败:${error}`, status: 'failed', runId: 'run', createdAt: 1200 }
  12. describe('conversation failure presentation', () => {
  13. it('folds diagnostics while preserving partial replies and user content', () => {
  14. expect(messageFailure(failed)).toMatchObject({ title: '响应超时', detail: error, replacesContent: true })
  15. expect(messageFailure({ ...failed, content: '已完成第一步。' })).toMatchObject({ replacesContent: false, detail: error })
  16. expect(messageFailure({ ...failed, role: 'user' })).toMatchObject({ title: '消息未发送', replacesContent: false })
  17. expect(messageFailure({ ...failed, error: undefined, content: '已完成第一步。' })).toMatchObject({ title: '回复暂时中断', replacesContent: false })
  18. expect(messageFailure({ ...failed, content: `Provider didn’t respond in time on any of 3 attempts — temporarily unavailable.\nProvider said: ${error}` })?.replacesContent).toBe(true)
  19. expect(messageFailure({ ...failed, status: 'settled', error: undefined })).toBeUndefined()
  20. expect(messageFailure({ ...failed, taskReference: { conversationId: 'c', taskId: 't' } })).toBeUndefined()
  21. })
  22. it('folds only a matching adjacent system failure from the same run', () => {
  23. expect(collapseDuplicateFailures([failed, duplicate])).toEqual([failed])
  24. for (const changed of [{ ...duplicate, runId: 'next' }, { ...duplicate, content: '执行失败:Other error' }, { ...duplicate, attachments: [{ id: 'f', name: '结果' }] }]) {
  25. expect(collapseDuplicateFailures([failed, changed])).toHaveLength(2)
  26. }
  27. expect(collapseDuplicateFailures([failed, { id: 'user', role: 'user', content: '重试' }, duplicate])).toHaveLength(3)
  28. expect(collapseDuplicateFailures([{ ...failed, runId: undefined }, { ...duplicate, runId: undefined }])).toHaveLength(1)
  29. expect(collapseDuplicateFailures([{ ...failed, runId: undefined }, { ...duplicate, runId: undefined, createdAt: 9000 }])).toHaveLength(2)
  30. expect(failed.content).toBe(error)
  31. })
  32. it('uses a native disclosure with the complete original diagnostic', () => {
  33. const wrapper = mount(MessageFailureNotice, { props: { failure: messageFailure(failed)! } })
  34. expect(wrapper.get('details').attributes('open')).toBeUndefined()
  35. expect(wrapper.get('summary').text()).toBe('响应超时')
  36. expect(wrapper.get('summary').text()).not.toContain('SSE')
  37. expect(wrapper.get('pre').text()).toBe(error)
  38. expect(wrapper.get('[role="status"]').text()).toBe('响应超时')
  39. wrapper.unmount()
  40. })
  41. it.each([MessageTimeline, WorkspaceMessageTimeline])('uses the same disclosure and keeps partial content in each timeline', async component => {
  42. vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} })
  43. Element.prototype.scrollTo = vi.fn()
  44. const wrapper = mount(component, { props: { messages: [failed, duplicate], identity: 'test' }, global: { stubs: { AgentAvatar: true, MarkdownContent: { props: ['content'], template: '<p class="answer">{{ content }}</p>' } } } })
  45. expect(wrapper.findAll('.failure-notice')).toHaveLength(1)
  46. expect(wrapper.findAll('.answer')).toHaveLength(0)
  47. await wrapper.setProps({ messages: [{ ...failed, content: '已完成第一步。' }] })
  48. expect(wrapper.get('.answer').text()).toBe('已完成第一步。')
  49. expect(wrapper.findAll('.failure-notice')).toHaveLength(1)
  50. wrapper.unmount()
  51. vi.unstubAllGlobals()
  52. })
  53. it('keeps workspace diagnostics available to the shared timeline', () => {
  54. const [message] = workspaceMessagesToUi([{ id: 'a', conversationId: 'c', seq: 1, role: 'assistant', content: '部分回复', reasoning: '', status: 'failed', error, runId: 'run', attachments: [], tools: [], createdAt: 1000 }])
  55. expect(message).toMatchObject({ error, runId: 'run' })
  56. })
  57. })