feat: extract text from fetched PDFs

This commit is contained in:
2026-11-01 14:20:30 -08:00
parent a390c7287a
commit cb9d5a787b
13 changed files with 768 additions and 43 deletions
+63
View File
@@ -0,0 +1,63 @@
import assert from "node:assert/strict";
import test from "node:test";
import { extractPdfText, hasPdfMagic } from "../src/llm/pdf-text.js";
import { createPdf } from "./pdf-fixture.js";
test("extracts selectable text with page markers and selected-page support", async () => {
const bytes = createPdf(["Quarterly revenue was 42.", "Costs were 18."]);
const result = await extractPdfText(bytes, [2]);
assert.equal(result.status, "ok");
assert.equal(result.pageCount, 2);
assert.deepEqual(result.pages, [2]);
assert.match(result.text, /--- Page 2 ---/);
assert.match(result.text, /Costs were 18\./);
assert.doesNotMatch(result.text, /Quarterly revenue/);
});
test("reports PDFs with no selectable text clearly", async () => {
const result = await extractPdfText(createPdf([""]));
assert.equal(result.status, "no_text");
assert.match(result.text, /No extractable text/);
});
test("reports encrypted PDFs clearly", async () => {
const result = await extractPdfText(createPdf(["Secret"], true));
assert.equal(result.status, "encrypted");
assert.match(result.text, /encrypted and requires a password/);
});
test("reports page-free PDFs and rejects invalid requested pages", async () => {
const empty = await extractPdfText(createPdf([]));
assert.equal(empty.status, "no_pages");
assert.match(empty.text, /no pages/);
const outOfRange = await extractPdfText(createPdf(["Only page"]), [2]);
assert.equal(outOfRange.status, "error");
assert.match(outOfRange.text, /has 1 pages/);
});
test("limits each extraction to 30 pages and preserves the full page count", async () => {
const result = await extractPdfText(createPdf(Array.from({ length: 31 }, (_, index) => `Text on page ${index + 1}.`)));
assert.equal(result.status, "ok");
assert.equal(result.pageCount, 31);
assert.equal(result.pages.length, 30);
assert.equal(result.pages.at(-1), 30);
});
test("caps accumulated extracted text at the caller limit", async () => {
const result = await extractPdfText(createPdf(["Long sentence. ".repeat(200)]), undefined, 50);
assert.equal(result.status, "ok");
assert.equal(result.truncated, true);
assert.ok(result.text.length <= 50);
});
test("terminates an over-time worker and returns an explicit timeout result", async () => {
const result = await extractPdfText(createPdf(["Small document"]), undefined, 12_000, 1);
assert.equal(result.status, "error");
assert.match(result.text, /time limit/);
});
test("recognizes PDF magic in a mislabeled or prefixed response", () => {
assert.equal(hasPdfMagic(new TextEncoder().encode("%PDF-1.4")), true);
assert.equal(hasPdfMagic(new TextEncoder().encode(`${" ".repeat(500)}%PDF-1.7`)), true);
assert.equal(hasPdfMagic(new TextEncoder().encode("<html>not a PDF</html>")), false);
});