messageFiles.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. // @vitest-environment node
  2. import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { expect, it, vi } from 'vitest'
  6. import { WorkspaceStore } from '../../src/server/workspaceStore'
  7. import { WorkspaceAssets, type StoredWorkspaceFile } from '../../src/server/workspaceAssets'
  8. import type { WorkspaceNodes } from '../../src/server/workspaceGateway'
  9. import type { WorkspaceMessage, WorkspaceConversation } from '../../src/shared/workspace'
  10. import { messageFileReferences, nativeMessageFileText } from '../../src/shared/messageFiles'
  11. import { saveFileAccess } from '../../src/server/fileAccess'
  12. it('archives only message references and reconciles legacy associations without deleting bytes or logs', async () => {
  13. const home = mkdtempSync(join(tmpdir(), 'yaoyao-message-files-'))
  14. const store = new WorkspaceStore(home)
  15. const request = vi.fn(async (_route: string, _options: { search: URLSearchParams }) => ({ status: 200, body: Buffer.from('report'), headers: new Headers({ 'content-type': 'text/plain' }) }))
  16. saveFileAccess(home, { mode: 'all', folders: [] })
  17. const assets = new WorkspaceAssets(store, { requireSource: () => {}, target: () => ({ session: { request } }) } as unknown as WorkspaceNodes, home)
  18. try {
  19. const agent = store.createAgent('owner', { name: 'Agent', profile: 'default' })
  20. const conversation = store.list<WorkspaceConversation>('owner', 'conversation')[0]!
  21. const message: WorkspaceMessage = {
  22. id: 'reply', conversationId: conversation.id, agentId: agent.id, seq: 0, role: 'assistant',
  23. content: '<think>[hidden](/tmp/thinking.pdf)</think>[报告](sandbox:/tmp/report.txt)',
  24. reasoning: '[思考](/tmp/reasoning.pdf)', tools: [{ arguments: { path: '/tmp/input.pdf' }, result: { path: '/tmp/process.pdf' } }],
  25. status: 'complete', attachments: [], createdAt: Date.now(),
  26. }
  27. store.saveMessage('owner', message)
  28. await assets.archive('owner', message)
  29. expect(request).toHaveBeenCalledTimes(1)
  30. expect(request.mock.calls[0]?.[1]?.search.get('path')).toBe('/tmp/report.txt')
  31. // Reproduce an old archive which mixed a tool path into the message.
  32. await assets.archiveText('owner', '[old](/tmp/process.pdf)', 'local', 'default', conversation.id, message.id)
  33. const all = store.list<StoredWorkspaceFile>('owner', 'file')
  34. for (const file of all) {
  35. delete file.messageFileSource
  36. store.put('owner', 'file', file.id, file)
  37. }
  38. message.attachments = all
  39. store.saveMessage('owner', message)
  40. expect(store.messages('owner', conversation.id)[0]?.attachments.map(file => file.name)).toEqual(['report.txt'])
  41. expect(store.visibleFiles('owner').map(file => file.name)).toEqual(['report.txt'])
  42. const replay = store.events('owner', 0).filter(event => event.type === 'message.changed').at(-1)!
  43. expect((replay.data as WorkspaceMessage).attachments.map(file => file.name)).toEqual(['report.txt'])
  44. expect(store.get<WorkspaceMessage>('owner', 'message', message.id)?.tools).toEqual(message.tools)
  45. expect(all.every(file => readFileSync(file.path, 'utf8') === 'report')).toBe(true)
  46. message.content += '\n[交付过程文件](/tmp/process.pdf)'
  47. store.saveMessage('owner', message)
  48. expect(store.visibleFiles('owner')).toHaveLength(2)
  49. expect(store.visibleFiles('another-owner')).toEqual([])
  50. } finally { assets.close(); store.close(); rmSync(home, { recursive: true, force: true }) }
  51. })
  52. it('does not scan native reasoning, tool data, code examples or unfinished thinking', () => {
  53. const text = nativeMessageFileText({
  54. text: '[交付](/tmp/report.pdf)', reasoning: '/tmp/reasoning.pdf',
  55. tools: [{ result: { path: '/tmp/process.pdf' } }],
  56. attachments: [{ path: '/tmp/attached.pdf', name: 'attached.pdf' }],
  57. })
  58. expect([...messageFileReferences(text)]).toEqual(['/tmp/report.pdf', '/tmp/attached.pdf'])
  59. expect(nativeMessageFileText({ role: 'tool', content: '[过程](/tmp/process.pdf)' })).toBe('')
  60. expect([...messageFileReferences('```json\n{"path":"/tmp/process.pdf"}\n```\n<think>![图](/tmp/hidden.png)')]).toEqual([])
  61. })