From 1cdfe9ed7d7b7f2575916e514b94f9cf8976e499 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 4 Aug 2026 07:02:00 +0800 Subject: [PATCH] test(qa): cover PDF extraction dispatch (#118860) --- .../pdf-document-extraction-dispatch.yaml | 28 +++++ ...cument-extraction-dispatch.product.test.ts | 109 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 qa/scenarios/media/pdf-document-extraction-dispatch.yaml create mode 100644 test/e2e/qa-lab/media/pdf-document-extraction-dispatch.product.test.ts diff --git a/qa/scenarios/media/pdf-document-extraction-dispatch.yaml b/qa/scenarios/media/pdf-document-extraction-dispatch.yaml new file mode 100644 index 000000000000..74542e2790a2 --- /dev/null +++ b/qa/scenarios/media/pdf-document-extraction-dispatch.yaml @@ -0,0 +1,28 @@ +title: PDF document extraction dispatch + +scenario: + id: pdf-document-extraction-dispatch + surface: media + category: media.media-intake-and-access + coverage: + primary: + - media.pdf-document-extraction-dispatch + objective: Verify PDF intake dispatches through the bundled document extractor to real clawpdf and reports disabled extraction explicitly. + successCriteria: + - The bundled document-extract plugin supplies the PDF extractor. + - A parseable inline PDF yields its sentinel text through real clawpdf. + - The document extractor runtime tags the result with extractor pdf. + - Disabling document extraction produces the explicit unavailable error. + docsRefs: + - docs/plugins/reference/document-extract.md + - docs/tools/pdf.md + codeRefs: + - src/media/pdf-extract.ts + - src/media/document-extractors.runtime.ts + - src/plugins/document-extractors.runtime.ts + - extensions/document-extract/document-extractor.ts + - test/e2e/qa-lab/media/pdf-document-extraction-dispatch.product.test.ts + execution: + kind: vitest + path: test/e2e/qa-lab/media/pdf-document-extraction-dispatch.product.test.ts + summary: Run a parseable inline PDF through bundled extractor discovery, real clawpdf extraction, and the disabled-plugin failure path. diff --git a/test/e2e/qa-lab/media/pdf-document-extraction-dispatch.product.test.ts b/test/e2e/qa-lab/media/pdf-document-extraction-dispatch.product.test.ts new file mode 100644 index 000000000000..82d8d0208d7b --- /dev/null +++ b/test/e2e/qa-lab/media/pdf-document-extraction-dispatch.product.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from "vitest"; +import { extractDocumentContent } from "../../../../src/media/document-extractors.runtime.js"; +import { extractPdfContent } from "../../../../src/media/pdf-extract.js"; +import { resolvePluginDocumentExtractors } from "../../../../src/plugins/document-extractors.runtime.js"; + +const SENTINEL = "OPENCLAW_PDF_DISPATCH_SENTINEL"; + +function createInlinePdf(text: string): Buffer { + const escapedText = text.replace(/[\\()]/gu, "\\$&"); + const content = `BT +/F1 18 Tf +72 720 Td +(${escapedText}) Tj +ET +`; + const objects = [ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + `<< /Length ${Buffer.byteLength(content)} >> +stream +${content}endstream`, + ]; + + let pdf = "%PDF-1.4\n% OpenClaw QA fixture\n"; + const offsets: number[] = []; + for (const [index, object] of objects.entries()) { + offsets.push(Buffer.byteLength(pdf)); + pdf += `${index + 1} 0 obj +${object} +endobj +`; + } + + const xrefOffset = Buffer.byteLength(pdf); + pdf += `xref +0 ${objects.length + 1} +`; + pdf += "0000000000 65535 f \n"; + for (const offset of offsets) { + pdf += `${String(offset).padStart(10, "0")} 00000 n \n`; + } + pdf += `trailer +<< /Size ${objects.length + 1} /Root 1 0 R >> +startxref +${xrefOffset} +%%EOF +`; + return Buffer.from(pdf, "ascii"); +} + +describe("PDF document extraction dispatch", () => { + it("uses the bundled PDF extractor and reports disabled extraction explicitly", async () => { + const buffer = createInlinePdf(SENTINEL); + const bundledPdfExtractor = resolvePluginDocumentExtractors().find( + (extractor) => extractor.id === "pdf", + ); + + expect(bundledPdfExtractor).toMatchObject({ + id: "pdf", + pluginId: "document-extract", + mimeTypes: ["application/pdf"], + }); + + const extracted = await extractDocumentContent({ + buffer, + mimeType: "application/pdf", + maxPages: 1, + maxPixels: 1_000_000, + minTextChars: 1, + }); + expect(extracted).toMatchObject({ + extractor: "pdf", + text: SENTINEL, + images: [], + }); + + await expect( + extractPdfContent({ + buffer, + maxPages: 1, + maxPixels: 1_000_000, + minTextChars: 1, + }), + ).resolves.toEqual({ + text: SENTINEL, + images: [], + }); + + await expect( + extractPdfContent({ + buffer, + maxPages: 1, + maxPixels: 1_000_000, + minTextChars: 1, + config: { + plugins: { + entries: { + "document-extract": { enabled: false }, + }, + }, + }, + }), + ).rejects.toThrow( + "PDF extraction disabled or unavailable: enable the document-extract plugin to process application/pdf files.", + ); + }); +});