import assert from "node:assert/strict"; import { access, lstat, mkdir, mkdtemp, readFile, stat, symlink, utimes, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { OcrClient, OcrClientError, type OcrResult } from "../../src/modules/ocr/client.js"; import { persistComposedCandidateArtifact, persistOcrResultArtifact, readComposedCandidateArtifact, readOcrResultArtifact, resolveArtifactPath, stageOcrArtifacts, sweepOrphanArtifacts, transferReviewImageArtifacts } from "../../src/modules/ocr/artifacts.js"; import { canonicalJson, hashOrderedPairs, sha256Hex } from "../../src/shared/utils/ids.js"; const document = Buffer.from("%PDF-1.4\nunit-7\n%%EOF\n"); const documentSha256 = sha256Hex(document); const jobId = "ocr_job-7"; function jsonResponse(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); } function ack(overrides: Record = {}): Record { return { jobId, status: "queued", documentSha256, requestedPages: [1, 3], configVersion: "ocr-v1", createdAt: "2026-09-14T10:00:00.000Z", ...overrides }; } function result(overrides: Record = {}): Record { return { schemaVersion: "1", jobId, documentSha256, engine: { name: "paddleocr", version: "3.4.0", runtime: "paddlepaddle-3.2.2", device: "cpu", configVersion: "ocr-v1", dpi: 200 }, pages: [1, 3].map((page) => ({ page, width: 1700, height: 2200, processingMs: 25, text: `page ${page}`, metrics: { lineCount: 1, nonWhitespaceCharacters: 5, inkCoverage: 0.3, medianConfidence: 0.95, p10Confidence: 0.95, lowConfidenceLineRatio: 0 }, lines: [{ lineId: `p${page}-l1`, text: `page ${page}`, confidence: 0.95, bbox: [1, 2, 3, 4] }] })), ...overrides }; } test("runtime retry stub preserves identity, exact attempts, backoff, and result integrity", async () => { const calls: Array<{ url: string; key: string | null }> = []; const delays: number[] = []; const responses: Array = [ new TypeError("connection reset"), jsonResponse(503, { detail: { code: "ENGINE_UNAVAILABLE" } }), jsonResponse(202, ack()), jsonResponse(200, result()) ]; const client = new OcrClient({ baseUrl: "http://ocr.internal:8000/", token: "internal-test-token", fetch: (async (input, init) => { calls.push({ url: String(input), key: new Headers(init?.headers).get("Idempotency-Key") }); const response = responses.shift(); if (response instanceof Error) throw response; return response as Response; }) as typeof fetch, sleep: async (milliseconds) => { delays.push(milliseconds); } }); const submitted = await client.submit(document, { documentSha256, pages: [1, 3] }); const completed = await client.getResult(jobId, { documentSha256, pages: [1, 3] }); assert.equal(submitted.jobId, jobId); assert.deepEqual(delays, [2_000, 4_000]); assert.equal(calls.filter(({ url }) => url.endsWith("/v1/jobs")).length, 3); assert.equal(new Set(calls.slice(0, 3).map(({ key }) => key)).size, 1); assert.equal(calls[0]?.key, `${documentSha256}:ocr-v1:${sha256Hex("[1,3]")}`); assert.deepEqual(completed.pages.map(({ page }) => page), [1, 3]); }); test("queue pressure and deterministic failures are surfaced without retries", async () => { for (const [status, retryable] of [[429, true], [422, false]] as const) { let attempts = 0; const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token", fetch: (async () => { attempts += 1; return jsonResponse(status, { detail: { code: status === 429 ? "QUEUE_FULL" : "UNSUPPORTED_PDF" } }); }) as typeof fetch, sleep: async () => { throw new Error("must not sleep"); } }); await assert.rejects( client.submit(document, { documentSha256, pages: [1, 3] }), (error: unknown) => error instanceof OcrClientError && error.status === status && error.retryable === retryable ); assert.equal(attempts, 1); } }); test("strict validation rejects mismatched acknowledgements and result contracts", async () => { for (const response of [ jsonResponse(202, ack({ requestedPages: [3, 1] })), jsonResponse(200, result({ schemaVersion: "2" })), jsonResponse(200, result({ pages: [result().pages as unknown] })) ]) { const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token", fetch: (async () => response) as typeof fetch, sleep: async () => undefined }); const operation = response.status === 202 ? client.submit(document, { documentSha256, pages: [1, 3] }) : client.getResult(jobId, { documentSha256, pages: [1, 3] }); await assert.rejects(operation, /OCR response integrity validation failed/); } }); test("polling uses bounded exponential backoff until a terminal status", async () => { const delays: number[] = []; const states = ["queued", "running", "succeeded"] as const; const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token", fetch: (async () => jsonResponse(200, { jobId, status: states.shift(), completedPages: states.length === 0 ? 2 : 0, totalPages: 2, error: null })) as typeof fetch, sleep: async (milliseconds) => { delays.push(milliseconds); } }); assert.equal((await client.pollUntilTerminal(jobId)).status, "succeeded"); assert.deepEqual(delays, [2_000, 4_000]); }); test("authenticated delete accepts an empty idempotent response", async () => { let request: { url: string; method: string; authorization: string | null } | undefined; const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "delete-token", fetch: (async (input, init) => { request = { url: String(input), method: init?.method ?? "GET", authorization: new Headers(init?.headers).get("Authorization") }; return new Response(null, { status: 204 }); }) as typeof fetch }); await client.delete("job / 1"); assert.deepEqual(request, { url: "http://ocr.internal:8000/v1/jobs/job%20%2F%201", method: "DELETE", authorization: "Bearer delete-token" }); }); test("review image transfer validates authentication, identity, content type, and hash", async () => { const png = Buffer.from("89504e470d0a1a0a0102", "hex"); const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "image-token", fetch: (async (_input, init) => new Response(png, { status: 200, headers: { "content-type": "image/png", "content-length": String(png.length), "x-document-sha256": documentSha256, "x-content-sha256": sha256Hex(png), "x-page-number": "1", "x-seen-authorization": new Headers(init?.headers).get("authorization") ?? "" } })) as typeof fetch }); assert.deepEqual(await client.getReviewImage(jobId, 1, documentSha256), { bytes: png, sha256: sha256Hex(png) }); await assert.rejects(client.getReviewImage(jobId, 2, documentSha256), /integrity validation failed/); }); test("review image transfer treats size and HTTP 500 as terminal while retrying transient failures", async () => { const png = Buffer.from("89504e470d0a1a0a0102", "hex"); for (const [status, expectedAttempts, retryable] of [[500, 1, false], [503, 3, true]] as const) { let attempts = 0; const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token", fetch: (async () => { attempts += 1; return new Response(null, { status }); }) as typeof fetch, sleep: async () => undefined }); await assert.rejects(client.getReviewImage(jobId, 1, documentSha256), (error: unknown) => error instanceof OcrClientError && error.status === status && error.retryable === retryable); assert.equal(attempts, expectedAttempts); } const invalidSize = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token", fetch: (async () => new Response(png, { headers: { "content-type": "image/png", "content-length": String(png.length + 1), "x-document-sha256": documentSha256, "x-content-sha256": sha256Hex(png), "x-page-number": "1" } })) as typeof fetch }); await assert.rejects(invalidSize.getReviewImage(jobId, 1, documentSha256), /integrity validation failed/); }); test("review image transfer is sequential and resumes from durable per-page checkpoints", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-image-transfer-")); context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true }))); const versionId = "12121212-1212-4121-8121-121212121212"; const documentId = "doc:image-transfer"; await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-21T10:00:00.000Z", documents: [{ documentId, documentKey: "scan.pdf", bytes: document, requestedPages: [1, 2, 3], pages: [1, 2, 3].map((page) => ({ page, text: "", rasterCoverage: 1, textSha256: sha256Hex("") })) }] }); const calls: number[] = []; let active = 0; let maxActive = 0; let interrupted = true; const loadImage = async (page: number) => { calls.push(page); active += 1; maxActive = Math.max(maxActive, active); await Promise.resolve(); active -= 1; if (page === 2 && interrupted) throw new Error("transfer interrupted"); const bytes = Buffer.from(`89504e470d0a1a0a${String(page).padStart(4, "0")}`, "hex"); return { bytes, sha256: sha256Hex(bytes) }; }; await assert.rejects(transferReviewImageArtifacts({ rootDirectory, versionId, documentId, loadImage }), /transfer interrupted/); assert.deepEqual(calls, [1, 2]); interrupted = false; const transferred = await transferReviewImageArtifacts({ rootDirectory, versionId, documentId, loadImage }); assert.deepEqual(calls, [1, 2, 2, 3]); assert.equal(maxActive, 1); assert.deepEqual(transferred.images.map(({ page }) => page), [1, 2, 3]); assert.deepEqual(await transferReviewImageArtifacts({ rootDirectory, versionId, documentId, loadImage }), transferred); assert.deepEqual(calls, [1, 2, 2, 3]); await writeFile(transferred.images[0]!.artifactPath, "corrupt", { mode: 0o600 }); await assert.rejects(transferReviewImageArtifacts({ rootDirectory, versionId, documentId, loadImage }), /checkpoint integrity validation failed/); assert.deepEqual(calls, [1, 2, 2, 3]); }); test("artifact staging writes private originals and a verifiable canonical manifest", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-artifacts-")); context.after(async () => { await import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })); }); const versionId = "11111111-1111-4111-8111-111111111111"; const staged = await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-14T10:00:00.000Z", documents: [ { documentId: "doc:source:b", documentKey: "b.pdf", bytes: Buffer.from("%PDF-b") }, { documentId: "doc:source:a", documentKey: "a.pdf", bytes: Buffer.from("%PDF-a") } ] }); const persisted = JSON.parse(await readFile(staged.manifestPath, "utf8")); assert.equal(staged.originalManifestHash, hashOrderedPairs([["b.pdf", sha256Hex("%PDF-b")], ["a.pdf", sha256Hex("%PDF-a")]])); assert.equal(staged.manifestSha256, sha256Hex(canonicalJson(persisted))); assert.deepEqual(persisted.documents.map((entry: { documentKey: string }) => entry.documentKey), ["a.pdf", "b.pdf"]); for (const entry of persisted.documents) { const originalPath = resolveArtifactPath(staged.versionDirectory, entry.originalPath); assert.equal((await stat(originalPath)).mode & 0o777, 0o600); assert.equal(sha256Hex(await readFile(originalPath)), entry.originalSha256); } assert.equal((await stat(staged.manifestPath)).mode & 0o777, 0o600); }); test("durable OCR result survives simulated remote deletion with exact restart readback", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-result-")); context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true }))); const versionId = "66666666-6666-4666-8666-666666666666"; const documentId = "doc:durable-result"; await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-16T10:00:00.000Z", documents: [{ documentId, documentKey: "scan.pdf", bytes: document }] }); let remoteResult: OcrResult | undefined = result() as unknown as OcrResult; const persisted = await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: remoteResult }); remoteResult = undefined; const restarted = await readOcrResultArtifact({ rootDirectory, versionId, documentId, jobId, documentSha256, pages: [1, 3], artifactSha256: persisted.artifactSha256 }); const repeated = await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: restarted }); assert.equal(remoteResult, undefined); assert.deepEqual(restarted, result()); assert.equal(repeated.artifactPath, persisted.artifactPath); assert.equal(repeated.artifactSha256, persisted.artifactSha256); assert.equal((await stat(persisted.artifactPath)).mode & 0o777, 0o600); }); test("OCR result readback fails closed on corruption and artifact identity mismatch", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-corrupt-")); context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true }))); const versionId = "77777777-7777-4777-8777-777777777777"; const documentId = "doc:corruption"; await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-16T10:00:00.000Z", documents: [{ documentId, documentKey: "scan.pdf", bytes: document }] }); const persisted = await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: result() as unknown as OcrResult }); const envelope = JSON.parse(await readFile(persisted.artifactPath, "utf8")) as Record; await writeFile(persisted.artifactPath, canonicalJson({ ...envelope, documentId: "doc:other" }), { mode: 0o600 }); await assert.rejects(readOcrResultArtifact({ rootDirectory, versionId, documentId, jobId, documentSha256, pages: [1, 3] }), /identity validation failed/); await writeFile(persisted.artifactPath, "{corrupt", { mode: 0o600 }); await assert.rejects(readOcrResultArtifact({ rootDirectory, versionId, documentId, jobId, documentSha256, pages: [1, 3] }), /integrity validation failed/); }); test("restart-safe candidate composition persists exact native, OCR, and blank page evidence", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-candidate-")); context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true }))); const versionId = "88888888-8888-4888-8888-888888888888"; const documentId = "doc:candidate"; await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-16T10:00:00.000Z", documents: [{ documentId, documentKey: "mixed.pdf", bytes: document, requestedPages: [2, 3], pages: [ { page: 1, text: "Native page", rasterCoverage: 0, textSha256: sha256Hex("Native page") }, { page: 2, text: "weak native", rasterCoverage: 0.8, textSha256: sha256Hex("weak native") }, { page: 3, text: "", rasterCoverage: 0, textSha256: sha256Hex("") } ] }] }); const ocrText = "codigo CBGO4a has enough OCR characters for the quality gate"; await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: result({ documentSha256, pages: [ { page: 2, width: 1700, height: 2200, processingMs: 10, text: ocrText, metrics: { lineCount: 1, nonWhitespaceCharacters: 50, inkCoverage: 0.4, medianConfidence: 0.95, p10Confidence: 0.95, lowConfidenceLineRatio: 0 }, lines: [{ lineId: "p2-l1", text: ocrText, confidence: 0.95, bbox: [1, 2, 3, 4] }] }, { page: 3, width: 1700, height: 2200, processingMs: 10, text: "", metrics: { lineCount: 0, nonWhitespaceCharacters: 0, inkCoverage: 0.001, medianConfidence: 0, p10Confidence: 0, lowConfidenceLineRatio: 0 }, lines: [] } ] }) as unknown as OcrResult }); const persisted = await persistComposedCandidateArtifact({ rootDirectory, versionId, jobs: [{ documentId, remoteJobId: jobId, requestedPages: [2, 3], state: "succeeded" }] }); const restarted = await readComposedCandidateArtifact({ rootDirectory, versionId, artifactSha256: persisted.artifactSha256 }); assert.deepEqual(restarted.documents[0]?.pages.map(({ method, candidateText }) => [method, candidateText]), [ ["native", "Native page"], ["ocr", ocrText], ["blank", ""] ]); assert.equal(restarted.documents[0]?.pages[1]?.risks[0], "CBGO4a"); assert.equal(restarted.candidateSha256, persisted.candidate.candidateSha256); assert.equal((await stat(persisted.artifactPath)).mode & 0o777, 0o600); await writeFile(persisted.artifactPath, "{corrupt", { mode: 0o600 }); await assert.rejects(readComposedCandidateArtifact({ rootDirectory, versionId }), /candidate artifact integrity/); await assert.rejects(persistComposedCandidateArtifact({ rootDirectory, versionId, jobs: [{ documentId, remoteJobId: jobId, requestedPages: [2, 3], state: "succeeded" }] }), /conflicts with durable content/); await writeFile(persisted.artifactPath, canonicalJson(restarted), { mode: 0o600 }); const manifest = JSON.parse(await readFile(path.join(rootDirectory, versionId, "manifest.json"), "utf8")) as { documents: Array<{ nativePagesPath: string }> }; await import("node:fs/promises").then(({ rm }) => rm(path.join(path.dirname(resolveArtifactPath(path.join(rootDirectory, versionId), manifest.documents[0]!.nativePagesPath)), "ocr-result.json"))); await assert.rejects(persistComposedCandidateArtifact({ rootDirectory, versionId, jobs: [{ documentId, remoteJobId: jobId, requestedPages: [2, 3], state: "succeeded" }] }), /result artifact integrity/); }); test("candidate composition fails closed on quality failure and corrupt native evidence", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-candidate-fail-")); context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true }))); const versionId = "99999999-9999-4999-8999-999999999999"; const documentId = "doc:blocked"; const staged = await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-16T10:00:00.000Z", documents: [{ documentId, documentKey: "scan.pdf", bytes: document, requestedPages: [1], pages: [{ page: 1, text: "", rasterCoverage: 1, textSha256: sha256Hex("") }] }] }); await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: result({ pages: [{ page: 1, width: 1700, height: 2200, processingMs: 10, text: "", lines: [], metrics: { lineCount: 0, nonWhitespaceCharacters: 0, inkCoverage: 0.5, medianConfidence: 0, p10Confidence: 0, lowConfidenceLineRatio: 0 } }] }) as unknown as OcrResult }); const jobs = [{ documentId, remoteJobId: jobId, requestedPages: [1], state: "succeeded" as const }]; await assert.rejects(persistComposedCandidateArtifact({ rootDirectory, versionId, jobs }), /OCR_QUALITY_BLOCKED/); const manifest = JSON.parse(await readFile(staged.manifestPath, "utf8")) as { documents: Array<{ nativePagesPath: string }> }; await writeFile(resolveArtifactPath(staged.versionDirectory, manifest.documents[0]!.nativePagesPath), "{corrupt", { mode: 0o600 }); await assert.rejects(persistComposedCandidateArtifact({ rootDirectory, versionId, jobs }), /native page artifact integrity/); }); test("safe resolution blocks traversal and orphan sweep preserves retained, recent, and non-directory entries", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-sweep-")); context.after(async () => { await import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })); }); const retained = "22222222-2222-4222-8222-222222222222"; const orphan = "33333333-3333-4333-8333-333333333333"; const recent = "44444444-4444-4444-8444-444444444444"; await Promise.all([retained, orphan, recent].map((id) => mkdir(path.join(rootDirectory, id)))); await utimes(path.join(rootDirectory, orphan), new Date(0), new Date(0)); await symlink(path.join(rootDirectory, orphan), path.join(rootDirectory, "55555555-5555-4555-8555-555555555555")); assert.throws(() => resolveArtifactPath(path.join(rootDirectory, retained), "../manifest.json"), /escapes version directory/); assert.deepEqual(await sweepOrphanArtifacts({ rootDirectory, retainedVersionIds: new Set([retained]), olderThan: new Date("2026-09-14T09:00:00.000Z") }), [orphan]); await assert.rejects(access(path.join(rootDirectory, orphan))); await Promise.all([retained, recent].map((id) => access(path.join(rootDirectory, id)))); assert.equal((await lstat(path.join(rootDirectory, "55555555-5555-4555-8555-555555555555"))).isSymbolicLink(), true); });