Add HTTP PaddleOCR-VL settings and shared document previews for chat and knowledge imports. Preserve attachment originals and parsed assets across drafts, messages and queues; support explicit image OCR, selecting document images into a conversation draft, and restoring queued input for editing. Protect unsaved note bodies and titles during navigation and asynchronous updates. Match canvas analysis to source revisions, invalidate affected comments, preserve annotations during pagination, and provide a resizable record index with saved width preferences. Existing OCR settings retain local behavior. Historical attachments without saved originals are not reconstructed. Advanced OCR options depend on the configured service; image sending depends on the effective model and Runtime. Validation: full typecheck and lint passed; 477 focused tests passed across 12 files; staged whitespace checks passed. The full repository suite was not rerun for this commit. Release note: Keep note drafts safe while switching workspaces, resize the record index, and revisit saved document attachments with OCR previews and selected images in chat.
88 lines
5.1 KiB
TypeScript
88 lines
5.1 KiB
TypeScript
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { randomUUID } from 'node:crypto'
|
|
import { DatabaseSync } from 'node:sqlite'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { ConversationAttachmentStorage } from './conversation-attachment-storage'
|
|
import { DocumentResultStorage } from './document-result-storage'
|
|
import { defaultDocumentParsingSettings } from './document-parsing-settings-store'
|
|
|
|
describe('conversation attachment files and references', () => {
|
|
it('retains interrupted originals without turning them into sendable draft attachments', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-attachment-'))
|
|
let results = new DocumentResultStorage(join(root, 'temp', 'document-parsing'))
|
|
let storage = new ConversationAttachmentStorage(root, results)
|
|
try {
|
|
const conversation = randomUUID()
|
|
const bytes = Buffer.from('synthetic pending source')
|
|
const id = storage.beginParsing(conversation, 'pending.pdf', bytes)
|
|
storage.close()
|
|
results = new DocumentResultStorage(join(root, 'temp', 'document-parsing'))
|
|
storage = new ConversationAttachmentStorage(root, results)
|
|
storage.reconcile((owner) => owner === conversation, true)
|
|
expect(storage.pendingParsing(conversation)).toMatchObject([{ id, parsingState: 'interrupted' }])
|
|
expect(storage.draft(conversation)).toEqual([])
|
|
expect(storage.original(id).data).toEqual(bytes)
|
|
storage.release('parsing', id)
|
|
storage.collect()
|
|
expect(storage.has(id)).toBe(false)
|
|
} finally { storage.close(); await results.close(); await rm(root, { recursive: true, force: true }) }
|
|
})
|
|
it('reopens originals and parsed documents after restart and retains shared references on deletion', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-attachment-'))
|
|
let results = new DocumentResultStorage(join(root, 'temp', 'document-parsing'))
|
|
let storage = new ConversationAttachmentStorage(root, results)
|
|
try {
|
|
const source = Buffer.from('original bytes')
|
|
const id = await storage.saveDocument('SOURCE.PDF', source, {
|
|
title: 'Source', sourceFormat: '.pdf', content: 'Parsed content', sections: [{ locator: 'Page 1', content: 'Parsed content' }],
|
|
warnings: [], parsingSettings: defaultDocumentParsingSettings
|
|
})
|
|
const context = { id, name: 'SOURCE.PDF', originalName: 'SOURCE.PDF', resourceId: id, resultId: id, kind: 'text' as const, size: 14, preview: 'Parsed content' }
|
|
storage.adoptDocument(context, JSON.stringify({ ...context, content: 'Parsed content' }))
|
|
const first = randomUUID(), second = randomUUID()
|
|
storage.reference(first, 'draft', first, [id])
|
|
storage.reference(second, 'message', randomUUID(), [id])
|
|
storage.close()
|
|
await results.close()
|
|
results = new DocumentResultStorage(join(root, 'temp', 'document-parsing'))
|
|
storage = new ConversationAttachmentStorage(root, results)
|
|
expect(storage.draft(first)).toEqual([context])
|
|
expect(storage.original(id).data.equals(source)).toBe(true)
|
|
expect((await results.get(id)).content).toBe('Parsed content')
|
|
storage.deleteConversation(first)
|
|
expect(storage.original(id).data.equals(source)).toBe(true)
|
|
const originalPath = storage.original(id).path
|
|
storage.deleteConversation(second)
|
|
expect(storage.has(id)).toBe(false)
|
|
await expect(readFile(originalPath)).rejects.toThrow()
|
|
} finally { storage.close(); await results.close(); await rm(root, { recursive: true, force: true }) }
|
|
})
|
|
|
|
it('keeps image bytes in files and preserves queue resources when a draft is cleared', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-attachment-'))
|
|
const results = new DocumentResultStorage(join(root, 'temp', 'document-parsing'))
|
|
const storage = new ConversationAttachmentStorage(root, results)
|
|
try {
|
|
const id = randomUUID(), conversation = randomUUID(), queue = randomUUID()
|
|
const image = Buffer.from('synthetic original image')
|
|
const context = { id, name: 'image.png', kind: 'image' as const, size: 4, preview: '4x4', thumbnailUrl: 'data:image/png;base64,AAAA', contentUrl: 'data:image/png;base64,BBBB', resourceId: id }
|
|
storage.save(context, JSON.stringify(context), image)
|
|
storage.reference(conversation, 'draft', conversation, [id])
|
|
storage.reference(conversation, 'queue', queue, [id])
|
|
storage.release('draft', conversation)
|
|
storage.collect()
|
|
expect(storage.original(id).data.equals(image)).toBe(true)
|
|
const database = new DatabaseSync(join(root, 'conversation-attachments.sqlite'))
|
|
try {
|
|
const row = database.prepare('SELECT metadata FROM attachments WHERE id = ?').get(id) as { metadata: string }
|
|
expect(row.metadata).not.toContain('base64')
|
|
} finally { database.close() }
|
|
storage.release('queue', queue)
|
|
storage.collect()
|
|
expect(storage.has(id)).toBe(false)
|
|
} finally { storage.close(); await results.close(); await rm(root, { recursive: true, force: true }) }
|
|
})
|
|
})
|