Files
goodbuddy/src/main/http-document-ocr.integration.test.ts
T
mesalogo a3b7a27045 fix: preserve drafts, canvas edits, and remote Continue tools
Keep paragraph formatting and checklist todos when removing canvas pages,
and preserve accepted undo/redo history after oversized input is rejected.
Merge restored queued text into the latest conversation draft and retain
image delivery when OCR returns image references and generated warnings only.

Deliver session MCP servers to bridged remote Continue Execute while keeping
Ask read-only and preserving local model-profile isolation. Add regression
coverage and update feature documentation. Correct stale conversation-limit
and forced-color assertions and the measured Electron layout test deadline.

Validation: 4,826 tests passed, 67 skipped; full typecheck and lint passed.
Current-source Linux x64 Execute/Ask/Execute MCP delivery passed, using 10
real text-provider requests and no image-provider requests. Image generation
itself was not exercised. Staged whitespace checks passed before commit.

Release note: 修复恢复队列时覆盖新输入、画布删页丢失清单格式及超限输入清空撤销历史的问题;图片 OCR 无正文时保留原图发送方式,远程 Continue Execute 正确保留会话 MCP 工具。
2026-09-19 23:09:48 +08:00

106 lines
5.6 KiB
TypeScript

// @vitest-environment node
import { createServer } from 'node:http'
import { once } from 'node:events'
import { mkdtemp, rm } from 'node:fs/promises'
import type { AddressInfo } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createCanvas } from '@napi-rs/canvas'
import { expect, it } from 'vitest'
import { HttpDocumentOcr } from './http-document-ocr'
import { defaultHttpOcrSettings } from '../shared/document-parsing-contracts'
import { defaultDocumentParsingSettings } from './document-parsing-settings-store'
import { DocumentParsingService } from './document-parsing-service'
import { DocumentResultStorage } from './document-result-storage'
it('uses the production HTTP client for Bearer authentication, errors, cancellation and limits', async () => {
const calls: Array<{ authorization?: string; path?: string }> = []
const server = createServer(async (request, response) => {
calls.push({ authorization: request.headers.authorization, path: request.url })
for await (const _chunk of request) void _chunk
if (request.headers.authorization !== 'Bearer synthetic-test-key') { response.writeHead(401); response.end('fixture'); return }
if (request.url === '/health') { response.end('{}'); return }
if (request.url === '/openapi.json') { response.end(JSON.stringify({ components: { schemas: { InferRequest: { properties: { useChartRecognition: {} } } } } })); return }
if (request.url?.startsWith('/wait/')) { request.socket.once('close', () => response.destroy()); return }
response.setHeader('Content-Type', 'application/json')
response.end(JSON.stringify({ result: { layoutParsingResults: [{ prunedResult: {}, markdown: { text: 'Authenticated synthetic text', images: {} } }] } }))
})
server.listen(0, '127.0.0.1')
await once(server, 'listening')
const baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
try {
const settings = { ...defaultHttpOcrSettings, baseUrl, authentication: 'bearer' as const }
const client = new HttpDocumentOcr(settings, 'synthetic-test-key')
expect((await client.check()).declaredOptions).toEqual(['useChartRecognition'])
expect((await client.recognize(Buffer.from('fixture'), 1, [8])).sections[0]).toMatchObject({ pageNumber: 8, content: 'Authenticated synthetic text' })
await expect(new HttpDocumentOcr(settings, 'wrong-key').recognize(Buffer.from('fixture'), 1, [1])).rejects.toThrow('HTTP 401')
const controller = new AbortController()
const pending = new HttpDocumentOcr({ ...settings, baseUrl: `${baseUrl}/wait` }, 'synthetic-test-key').recognize(Buffer.from('fixture'), 1, [1], controller.signal)
const cancelled = expect(pending).rejects.toThrow('cancel fixture')
while (calls.length < 5) await new Promise(resolve => setTimeout(resolve, 5))
controller.abort(new Error('cancel fixture'))
await cancelled
expect(calls.filter(call => call.authorization === 'Bearer synthetic-test-key')).toHaveLength(4)
} finally {
server.closeAllConnections()
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
}
})
it.each([false, true])('classifies missing-image warnings consistently through HTTP parsing and storage (text=%s)', async (hasText) => {
const image = createCanvas(4, 4).toBuffer('image/png')
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-ocr-text-'))
const results = new DocumentResultStorage(directory)
const paths: string[] = []
const server = createServer(async (request, response) => {
for await (const chunk of request) void chunk
paths.push(request.url ?? '')
response.setHeader('Content-Type', 'application/json')
response.end(JSON.stringify({ result: { layoutParsingResults: [{
prunedResult: {},
markdown: {
text: `${hasText ? 'Recognized body\n' : ''}![valid](valid.png)\n![missing](missing.png)`,
images: { 'valid.png': image.toString('base64') }
}
}] } }))
})
server.listen(0, '127.0.0.1')
await once(server, 'listening')
try {
const settings = {
...defaultDocumentParsingSettings,
ocrProvider: 'paddleocr-vl' as const,
httpOcr: {
...defaultHttpOcrSettings,
baseUrl: `http://127.0.0.1:${(server.address() as AddressInfo).port}`
}
}
const parsed = await new HttpDocumentOcr(settings.httpOcr).recognize(image, 1, [1])
expect(parsed.images).toHaveLength(1)
expect(parsed.missingImages).toHaveLength(1)
const saved = await results.save('scan.png', image, {
...parsed, title: 'scan.png', sourceFormat: '.png',
content: parsed.sections.map(section => section.content).join('\n\n'),
pageCount: 1
}, settings, 0)
expect((await results.get(saved.id)).completeness).toBe(hasText ? 'partial' : 'images-only')
expect(saved.content).toContain('图片未保存')
const service = new DocumentParsingService({
forOperation: async () => ({ settings, apiKey: () => undefined })
} as never, {} as never, {} as never)
const extracted = service.extractImage('scan.png', image)
if (hasText) {
await expect(extracted).resolves.toMatchObject({ content: expect.stringContaining('Recognized body') })
} else {
await expect(extracted).rejects.toThrow('未提取到文字,原图片发送方式保持不变')
}
expect(paths).toEqual(['/layout-parsing', '/layout-parsing'])
} finally {
server.closeAllConnections()
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
await results.close()
await rm(directory, { recursive: true, force: true })
}
})