artifactStore.test.ts 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. import { describe, expect, it } from 'vitest'
  2. import type { ChatMessage, SessionSummary } from '@shared/types'
  3. import { extractArtifacts } from '@/utils/artifacts'
  4. const session: SessionSummary = {
  5. id: 'session-1', profile: 'yaoyao', source: 'web', title: '产物会话', messageCount: 4,
  6. toolCallCount: 1, startedAt: 1, updatedAt: 5,
  7. }
  8. function row(id: string, role: ChatMessage['role'], content: string, extras: Partial<ChatMessage> = {}): ChatMessage {
  9. return { id, sessionId: session.id, profile: session.profile, role, content, timestamp: 2, stage: 'settled', ...extras }
  10. }
  11. describe('artifact extractor', () => {
  12. it('extracts assistant markdown with readable link titles and ignores user rows', () => {
  13. const artifacts = extractArtifacts(session, [
  14. row('user', 'user', '[不要收集](https://private.example/user)'),
  15. row('assistant', 'assistant', '查看 [发布说明](https://example.com/releases/v1) 和 ![图](/tmp/chart.png)'),
  16. ])
  17. expect(artifacts.map(item => item.value)).toEqual(expect.arrayContaining(['https://example.com/releases/v1', '/tmp/chart.png']))
  18. expect(artifacts.find(item => item.kind === 'link')?.label).toBe('发布说明')
  19. expect(artifacts.find(item => item.kind === 'image')?.label).toBe('chart.png')
  20. })
  21. it('preserves structured attachment metadata for preview', () => {
  22. const artifacts = extractArtifacts(session, [row('assistant', 'assistant', '', {
  23. attachments: [{ id: 'attachment-1', name: 'demo.mp4', mimeType: 'video/mp4', size: 10, path: '/tmp/demo.mp4', kind: 'file' }],
  24. })])
  25. expect(artifacts[0]).toMatchObject({ kind: 'file', value: '/tmp/demo.mp4', mimeType: 'video/mp4' })
  26. expect(artifacts[0].attachment?.id).toBe('attachment-1')
  27. })
  28. it('excludes files mentioned only in tool results', () => {
  29. const artifacts = extractArtifacts(session, [row('tool', 'tool', '{"output_path":"/tmp/result.pdf"}', {
  30. toolCalls: [{ id: 'tool-1', name: 'write', status: 'completed', result: { output_path: '/tmp/result.pdf' } }],
  31. })])
  32. expect(artifacts).toEqual([])
  33. })
  34. it('collects the delivered body while ignoring reasoning and attached tool details', () => {
  35. const artifacts = extractArtifacts(session, [row('assistant', 'assistant',
  36. '<think>[过程](/tmp/private.pdf)</think>[交付](/tmp/result.pdf)', {
  37. reasoning: '![过程图片](/tmp/process.png)',
  38. toolCalls: [{ id: 'tool', name: 'read', status: 'completed', arguments: { path: '/tmp/input.pdf' }, result: { path: '/tmp/result.pdf' }, preview: '/tmp/preview.pdf' }],
  39. })])
  40. expect(artifacts.map(item => item.value)).toEqual(['/tmp/result.pdf'])
  41. expect(artifacts[0].messageId).toBe('assistant')
  42. })
  43. it('keeps the same artifact path when it was produced by distinct messages', () => {
  44. const artifacts = extractArtifacts(session, [
  45. row('assistant-1', 'assistant', '[第一版](/tmp/result.pdf)'),
  46. row('assistant-2', 'assistant', '[第二版](/tmp/result.pdf)', { timestamp: 3 }),
  47. ])
  48. expect(artifacts).toHaveLength(2)
  49. expect(artifacts.map(item => item.messageId)).toEqual(['assistant-1', 'assistant-2'])
  50. expect(new Set(artifacts.map(item => item.id)).size).toBe(2)
  51. })
  52. })