sessionOutline.test.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
  1. import { describe, expect, it } from 'vitest'
  2. import { mount } from '@vue/test-utils'
  3. import { nextTick } from 'vue'
  4. import type { UiMessage } from '@/components/messages/types'
  5. import MarkdownContent from '@/components/messages/MarkdownContent.vue'
  6. import { buildSessionOutline } from '@/utils/sessionOutline'
  7. describe('session outline', () => {
  8. it('keeps each user turn and all assistant H1-H3 headings before the next turn', () => {
  9. const messages: UiMessage[] = [
  10. { id: 'u1', role: 'user', content: '请给我一份完整方案' },
  11. { id: 'a1', role: 'assistant', content: '# 结论\n正文\n## 实施步骤' },
  12. { id: 'a2', role: 'assistant', content: '### 风险\n说明' },
  13. { id: 'u2', role: 'user', content: '再补充测试' },
  14. { id: 'a3', role: 'assistant', content: '## 测试计划' },
  15. ]
  16. expect(buildSessionOutline(messages).map(item => [item.type, item.level, item.content])).toEqual([
  17. ['user', 0, '请给我一份完整方案'],
  18. ['heading', 1, '结论'],
  19. ['heading', 2, '实施步骤'],
  20. ['heading', 3, '风险'],
  21. ['user', 0, '再补充测试'],
  22. ['heading', 2, '测试计划'],
  23. ])
  24. })
  25. it('ignores reasoning, fenced-code headings, tools, and timeline system events', () => {
  26. const messages: UiMessage[] = [
  27. { id: 'system', role: 'user', content: '模型切换', timelineKind: 'system' },
  28. { id: 'u1', role: 'user', content: '<think>内部</think>真实问题' },
  29. { id: 'a1', role: 'assistant', content: '<think># 隐藏标题</think>\n```md\n# 代码标题\n```\n## 可见标题' },
  30. { id: 'tool', role: 'tool', content: '# 工具标题' },
  31. ]
  32. expect(buildSessionOutline(messages).map(item => item.content)).toEqual(['真实问题', '可见标题'])
  33. })
  34. it('truncates long user questions to fifty characters', () => {
  35. const content = '这是一条很长的用户问题'.repeat(8)
  36. const [item] = buildSessionOutline([{ id: 'u1', role: 'user', content }])
  37. expect(item?.content).toHaveLength(51)
  38. expect(item?.content.endsWith('…')).toBe(true)
  39. })
  40. it('assigns stable DOM anchors to rendered Markdown headings', async () => {
  41. const wrapper = mount(MarkdownContent, { props: { content: '# 结论\n## 步骤\n### 风险', outlinePrefix: 'outline-a1' } })
  42. await nextTick()
  43. expect(wrapper.find('h1').attributes('id')).toBe('outline-a1-heading-1')
  44. expect(wrapper.find('h2').attributes('id')).toBe('outline-a1-heading-2')
  45. expect(wrapper.find('h3').attributes('id')).toBe('outline-a1-heading-3')
  46. })
  47. })