messageFiles.test.ts 3.9 KB

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