HTTP OCR failures in PPTX parsing were incorrectly labeled as local OCR failures. Report the selected provider and slide/image location, and retain bounded structured server diagnostics with configured-key redaction and log IDs. Preserve existing fallback behavior. Validation: 38 focused tests, typecheck and lint passed. Full test run timed out after 200 seconds. Synthetic service probes reproduced HTTP 500; the server-side failure remains unresolved. Release note: PPTX parsing warnings now identify the actual OCR provider and failed image location, with server diagnostics for troubleshooting.
106 lines
6.4 KiB
TypeScript
106 lines
6.4 KiB
TypeScript
import { createCanvas } from '@napi-rs/canvas'
|
||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||
import { defaultHttpOcrSettings } from '../shared/document-parsing-contracts'
|
||
import { HttpDocumentOcr } from './http-document-ocr'
|
||
|
||
afterEach(() => vi.unstubAllGlobals())
|
||
|
||
function page(text: string, images: Record<string, string> = {}) {
|
||
return { prunedResult: {}, markdown: { text, images } }
|
||
}
|
||
|
||
describe('HTTP document OCR', () => {
|
||
it('uses changed restructuring output with all source pages and retains page-local images', async () => {
|
||
const bytes = createCanvas(4, 4).toBuffer('image/png').toString('base64')
|
||
const fetch = vi.fn().mockResolvedValueOnce(Response.json({ result: { layoutParsingResults: [
|
||
page('first ', { a: bytes }), page('second')
|
||
] } })).mockResolvedValueOnce(Response.json({ result: { layoutParsingResults: [page('merged ', { a: bytes })] } }))
|
||
vi.stubGlobal('fetch', fetch)
|
||
const result = await new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test', mergeTables: true })
|
||
.recognize(Buffer.from('pdf'), 0, [5, 6])
|
||
expect(result.sections).toHaveLength(1)
|
||
expect(result.sections[0]).toMatchObject({ sourcePages: [5, 6], content: expect.stringContaining('merged') })
|
||
expect(result.sections[0]).not.toHaveProperty('pageNumber')
|
||
expect(result.images?.[0]?.pageNumber).toBe(5)
|
||
expect(result.restructure?.changed).toBe(true)
|
||
})
|
||
|
||
it('retains page results and reports a restructuring HTTP failure', async () => {
|
||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(Response.json({ result: { layoutParsingResults: [page('first'), page('second')] } }))
|
||
.mockResolvedValueOnce(new Response('', { status: 503 })))
|
||
const result = await new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test', mergeTables: true })
|
||
.recognize(Buffer.from('pdf'), 0, [1, 2])
|
||
expect(result.sections.map(section => section.content)).toEqual(['first', 'second'])
|
||
expect(result.warnings[0]).toContain('HTTP 503')
|
||
})
|
||
|
||
it('does not start restructuring when cancellation arrives after layout', async () => {
|
||
const controller = new AbortController()
|
||
const fetch = vi.fn(async () => { controller.abort(new Error('cancelled')); return Response.json({ result: { layoutParsingResults: [page('first')] } }) })
|
||
vi.stubGlobal('fetch', fetch)
|
||
await expect(new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test', mergeTables: true })
|
||
.recognize(Buffer.from('pdf'), 0, [1], controller.signal)).rejects.toThrow('cancelled')
|
||
expect(fetch).toHaveBeenCalledTimes(1)
|
||
})
|
||
it('preserves page-local image identity, rewrites HTML and Markdown, and omits confidence', async () => {
|
||
const bytes = createCanvas(4, 4).toBuffer('image/png').toString('base64')
|
||
const fetch = vi.fn(async () => Response.json({ result: { layoutParsingResults: [
|
||
page('<img src="imgs/a.png">', { 'imgs/a.png': bytes }),
|
||
page('', { 'imgs/a.png': bytes })
|
||
] } }))
|
||
vi.stubGlobal('fetch', fetch)
|
||
const provider = new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test', filterMode: 'all' })
|
||
const result = await provider.recognize(Buffer.from('pdf'), 0, [3, 4])
|
||
expect(result.images).toHaveLength(2)
|
||
expect(result.images![0]!.id).not.toBe(result.images![1]!.id)
|
||
expect(result.images!.map((image) => image.pageNumber)).toEqual([3, 4])
|
||
expect(result.sections[0]!.content).toContain(`asset:${result.images![0]!.id}`)
|
||
expect(result.sections[1]!.content).toContain(`asset:${result.images![1]!.id}`)
|
||
expect(result.sections[0]).not.toHaveProperty('confidence')
|
||
const request = (fetch.mock.calls as unknown as [string, RequestInit][])[0]![1]
|
||
expect(JSON.parse(request.body as string)).toMatchObject({ markdownIgnoreLabels: [], returnMarkdownImages: true, visualize: false })
|
||
})
|
||
|
||
it('does not fetch URL images and replaces broken references with an explicit warning', async () => {
|
||
const fetch = vi.fn(async () => Response.json({ result: { layoutParsingResults: [page('<img src="figure"> text', { figure: 'http://never.test/private' })] } }))
|
||
vi.stubGlobal('fetch', fetch)
|
||
const result = await new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test' })
|
||
.recognize(Buffer.from('image'), 1, [1])
|
||
expect(fetch).toHaveBeenCalledTimes(1)
|
||
expect(result.images).toEqual([])
|
||
expect(result.warnings[0]).toContain('未返回内联图片')
|
||
expect(result.sections[0]!.content).not.toContain('<img')
|
||
})
|
||
|
||
it.each([401, 429, 503])('preserves HTTP %s without returning provider secrets', async (status) => {
|
||
vi.stubGlobal('fetch', vi.fn(async () => new Response('secret response', { status })))
|
||
await expect(new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test' })
|
||
.recognize(Buffer.from('image'), 1, [1])).rejects.toThrow(`HTTP ${status}`)
|
||
})
|
||
|
||
it('does not dispatch after cancellation or with missing credentials', async () => {
|
||
const fetch = vi.fn()
|
||
vi.stubGlobal('fetch', fetch)
|
||
const provider = new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test', authentication: 'bearer' })
|
||
await expect(provider.recognize(Buffer.from('image'), 1, [1])).rejects.toThrow('凭据未配置')
|
||
await expect(provider.recognize(Buffer.from('image'), 1, [1], AbortSignal.abort())).rejects.toThrow()
|
||
expect(fetch).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it.each([200, 500])('retains structured provider diagnostics for status %s and redacts the configured key', async (status) => {
|
||
vi.stubGlobal('fetch', vi.fn(async () => Response.json({
|
||
errorCode: 500, errorMsg: 'invalid model settings test-secret', logId: 'probe-log-123'
|
||
}, { status })))
|
||
const client = new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test' }, 'test-secret')
|
||
await expect(client.recognize(Buffer.from('image'), 1, [1])).rejects.toThrow(
|
||
`${status === 500 ? 'HTTP 500;服务错误 500' : '服务错误 500'};invalid model settings [redacted];logId: probe-log-123`
|
||
)
|
||
})
|
||
|
||
it('preserves HTTP status for oversized error bodies', async () => {
|
||
vi.stubGlobal('fetch', vi.fn(async () => new Response('x'.repeat(9000), { status: 500 })))
|
||
await expect(new HttpDocumentOcr({ ...defaultHttpOcrSettings, baseUrl: 'http://ocr.test' })
|
||
.recognize(Buffer.from('image'), 1, [1])).rejects.toThrow('HTTP 500')
|
||
})
|
||
})
|